Scaffold browser capture extension
This commit is contained in:
parent
fecf61a375
commit
44ca183f50
|
|
@ -0,0 +1,2 @@
|
|||
dist/
|
||||
node_modules/
|
||||
65
README.md
65
README.md
|
|
@ -1,3 +1,66 @@
|
|||
# verstak-browser-extension
|
||||
|
||||
Verstak Browser Extension — Firefox/Chromium, page/text capture, link sending, pending queue, domain bindings
|
||||
Verstak Browser Extension captures pages, selected text, and links and sends
|
||||
them to a local Verstak browser inbox receiver.
|
||||
|
||||
The extension does not know Notes, Files, Activity, or Journal internals. It
|
||||
only sends capture events through the public local receiver protocol. If the
|
||||
receiver is offline, captures stay in the extension pending queue.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
npm ci
|
||||
npm test
|
||||
npm run build
|
||||
```
|
||||
|
||||
Build output:
|
||||
|
||||
- `dist/chromium`
|
||||
- `dist/firefox`
|
||||
|
||||
## Local Receiver Protocol
|
||||
|
||||
Default endpoint:
|
||||
|
||||
```text
|
||||
POST http://127.0.0.1:47731/api/browser-inbox/v1/captures
|
||||
```
|
||||
|
||||
Headers:
|
||||
|
||||
- `Content-Type: application/json`
|
||||
- `X-Verstak-Receiver-Token: <token>` optional, once pairing is implemented
|
||||
|
||||
Payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"captureId": "uuid-or-generated-id",
|
||||
"capturedAt": "2026-06-27T00:00:00.000Z",
|
||||
"source": "verstak-browser-extension",
|
||||
"kind": "page",
|
||||
"page": {
|
||||
"url": "https://example.com/article",
|
||||
"title": "Example Article",
|
||||
"domain": "example.com"
|
||||
},
|
||||
"browser": {
|
||||
"name": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Supported `kind` values:
|
||||
|
||||
- `page`
|
||||
- `selection`, with `selection.text`
|
||||
- `link`, with `link.url` and optional `link.text`
|
||||
|
||||
Expected success response:
|
||||
|
||||
```json
|
||||
{ "status": "accepted", "captureId": "uuid-or-generated-id" }
|
||||
```
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Verstak Capture",
|
||||
"version": "0.1.0",
|
||||
"description": "Send pages, selections, and links to the local Verstak browser inbox.",
|
||||
"permissions": ["contextMenus", "storage", "tabs"],
|
||||
"host_permissions": ["http://127.0.0.1/*", "http://localhost/*"],
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"action": {
|
||||
"default_popup": "popup/popup.html",
|
||||
"default_title": "Verstak Capture"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Verstak Capture",
|
||||
"version": "0.1.0",
|
||||
"description": "Send pages, selections, and links to the local Verstak browser inbox.",
|
||||
"permissions": ["contextMenus", "storage", "tabs", "http://127.0.0.1/*", "http://localhost/*"],
|
||||
"background": {
|
||||
"scripts": ["protocol.js", "api.js", "queue.js", "background.js"]
|
||||
},
|
||||
"browser_action": {
|
||||
"default_popup": "popup/popup.html",
|
||||
"default_title": "Verstak Capture"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "verstak-browser-extension",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "verstak-browser-extension",
|
||||
"version": "0.1.0",
|
||||
"devDependencies": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"name": "verstak-browser-extension",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Verstak browser capture extension for Chromium and Firefox",
|
||||
"scripts": {
|
||||
"build": "node scripts/build-extension.js",
|
||||
"test": "node scripts/test-protocol.js"
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
#!/usr/bin/env node
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const dist = path.join(root, 'dist');
|
||||
const shared = path.join(root, 'shared');
|
||||
|
||||
function rm(dir) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function mkdir(dir) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
function copy(src, dest) {
|
||||
mkdir(path.dirname(dest));
|
||||
fs.copyFileSync(src, dest);
|
||||
}
|
||||
|
||||
function concat(files, dest) {
|
||||
mkdir(path.dirname(dest));
|
||||
fs.writeFileSync(dest, files.map((file) => fs.readFileSync(file, 'utf8')).join('\n\n'), 'utf8');
|
||||
}
|
||||
|
||||
function copyPopup(destRoot) {
|
||||
const popupDir = path.join(shared, 'popup');
|
||||
for (const name of ['popup.html', 'popup.css', 'popup.js']) {
|
||||
copy(path.join(popupDir, name), path.join(destRoot, 'popup', name));
|
||||
}
|
||||
}
|
||||
|
||||
rm(dist);
|
||||
|
||||
const chromiumDist = path.join(dist, 'chromium');
|
||||
mkdir(chromiumDist);
|
||||
copy(path.join(root, 'chromium', 'manifest.json'), path.join(chromiumDist, 'manifest.json'));
|
||||
concat([
|
||||
path.join(shared, 'protocol.js'),
|
||||
path.join(shared, 'api.js'),
|
||||
path.join(shared, 'queue.js'),
|
||||
path.join(shared, 'background.js'),
|
||||
], path.join(chromiumDist, 'background.js'));
|
||||
copyPopup(chromiumDist);
|
||||
|
||||
const firefoxDist = path.join(dist, 'firefox');
|
||||
mkdir(firefoxDist);
|
||||
copy(path.join(root, 'firefox', 'manifest.json'), path.join(firefoxDist, 'manifest.json'));
|
||||
for (const name of ['protocol.js', 'api.js', 'queue.js', 'background.js']) {
|
||||
copy(path.join(shared, name), path.join(firefoxDist, name));
|
||||
}
|
||||
copyPopup(firefoxDist);
|
||||
|
||||
console.log('built dist/chromium and dist/firefox');
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
#!/usr/bin/env node
|
||||
const assert = require('assert');
|
||||
|
||||
const protocol = require('../shared/protocol');
|
||||
require('../shared/api');
|
||||
const queueApi = require('../shared/queue');
|
||||
|
||||
const page = protocol.buildCapture({
|
||||
kind: 'page',
|
||||
captureId: 'test-capture-id',
|
||||
url: 'https://example.com/docs',
|
||||
title: 'Example Docs'
|
||||
});
|
||||
assert.equal(page.schemaVersion, 1);
|
||||
assert.equal(page.captureId, 'test-capture-id');
|
||||
assert.equal(page.page.domain, 'example.com');
|
||||
assert.equal(protocol.validateCapture(page), true);
|
||||
|
||||
const selection = protocol.buildCapture({
|
||||
kind: 'selection',
|
||||
url: 'https://example.com/docs',
|
||||
title: 'Example Docs',
|
||||
selectionText: ' selected text '
|
||||
});
|
||||
assert.equal(selection.selection.text, 'selected text');
|
||||
assert.equal(protocol.validateCapture(selection), true);
|
||||
|
||||
assert.throws(() => protocol.validateCapture({ schemaVersion: 1, kind: 'link', captureId: 'x', capturedAt: 'now', page: { url: 'https://example.com' } }), /link.url/);
|
||||
|
||||
let request;
|
||||
const fetchOk = (url, options) => {
|
||||
request = { url, options };
|
||||
return Promise.resolve({ status: 202, json: () => Promise.resolve({ status: 'accepted' }) });
|
||||
};
|
||||
|
||||
globalThis.VerstakBrowser.sendCapture('http://127.0.0.1:47731/api/browser-inbox/v1/captures', 'token', page, fetchOk)
|
||||
.then((result) => {
|
||||
assert.equal(result.status, 'accepted');
|
||||
assert.equal(request.url, 'http://127.0.0.1:47731/api/browser-inbox/v1/captures');
|
||||
assert.equal(request.options.headers['X-Verstak-Receiver-Token'], 'token');
|
||||
assert.equal(JSON.parse(request.options.body).captureId, 'test-capture-id');
|
||||
})
|
||||
.then(() => {
|
||||
const queue = new queueApi.CaptureQueue(queueApi.createMemoryStorage());
|
||||
return queue.enqueue(page)
|
||||
.then(() => queue.enqueue(selection))
|
||||
.then(() => queue.retry((payload) => {
|
||||
if (payload.kind === 'page') return Promise.resolve();
|
||||
return Promise.reject(new Error('offline'));
|
||||
}))
|
||||
.then((result) => {
|
||||
assert.deepEqual(result, { sent: 1, pending: 1 });
|
||||
return queue.list();
|
||||
})
|
||||
.then((items) => {
|
||||
assert.equal(items.length, 1);
|
||||
assert.equal(items[0].kind, 'selection');
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
console.log('browser extension protocol tests passed');
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
(function (root) {
|
||||
'use strict';
|
||||
|
||||
function sendCapture(receiverUrl, token, payload, fetchImpl) {
|
||||
var protocol = root.VerstakBrowser || {};
|
||||
protocol.validateCapture(payload);
|
||||
var fetchFn = fetchImpl || root.fetch;
|
||||
if (typeof fetchFn !== 'function') return Promise.reject(new Error('fetch unavailable'));
|
||||
return fetchFn(receiverUrl || protocol.DEFAULT_RECEIVER_URL, {
|
||||
method: 'POST',
|
||||
headers: Object.assign({
|
||||
'Content-Type': 'application/json'
|
||||
}, token ? { 'X-Verstak-Receiver-Token': token } : {}),
|
||||
body: JSON.stringify(payload)
|
||||
}).then(function (response) {
|
||||
if (!response || response.status < 200 || response.status >= 300) {
|
||||
throw new Error('receiver rejected capture: HTTP ' + (response && response.status));
|
||||
}
|
||||
return response.json ? response.json() : { status: 'accepted', captureId: payload.captureId };
|
||||
});
|
||||
}
|
||||
|
||||
var api = { sendCapture: sendCapture };
|
||||
root.VerstakBrowser = Object.assign(root.VerstakBrowser || {}, api);
|
||||
if (typeof module !== 'undefined') module.exports = api;
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : this);
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
(function () {
|
||||
'use strict';
|
||||
|
||||
var ext = typeof browser !== 'undefined' ? browser : chrome;
|
||||
var protocol = globalThis.VerstakBrowser;
|
||||
var queue = new protocol.CaptureQueue(protocol.browserStorageAdapter(ext));
|
||||
var DEFAULT_SETTINGS = {
|
||||
receiverUrl: protocol.DEFAULT_RECEIVER_URL,
|
||||
receiverToken: ''
|
||||
};
|
||||
|
||||
function getSettings() {
|
||||
return ext.storage.local.get('settings').then(function (result) {
|
||||
return Object.assign({}, DEFAULT_SETTINGS, result && result.settings || {});
|
||||
});
|
||||
}
|
||||
|
||||
function activeTab() {
|
||||
return ext.tabs.query({ active: true, currentWindow: true }).then(function (tabs) {
|
||||
return tabs && tabs[0] || {};
|
||||
});
|
||||
}
|
||||
|
||||
function captureFromInfo(kind, info, tab) {
|
||||
return protocol.buildCapture({
|
||||
kind: kind,
|
||||
url: tab && tab.url || info.pageUrl || info.frameUrl || '',
|
||||
title: tab && tab.title || '',
|
||||
selectionText: info.selectionText || '',
|
||||
linkUrl: info.linkUrl || '',
|
||||
linkText: info.selectionText || ''
|
||||
});
|
||||
}
|
||||
|
||||
function sendOrQueue(payload) {
|
||||
return getSettings().then(function (settings) {
|
||||
return protocol.sendCapture(settings.receiverUrl, settings.receiverToken, payload).catch(function () {
|
||||
return queue.enqueue(payload).then(function () {
|
||||
return { status: 'queued', captureId: payload.captureId };
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function retryPending() {
|
||||
return getSettings().then(function (settings) {
|
||||
return queue.retry(function (payload) {
|
||||
return protocol.sendCapture(settings.receiverUrl, settings.receiverToken, payload);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setupContextMenus() {
|
||||
if (!ext.contextMenus) return;
|
||||
ext.contextMenus.removeAll(function () {
|
||||
ext.contextMenus.create({ id: 'verstak-capture-page', title: 'Send page to Verstak', contexts: ['page'] });
|
||||
ext.contextMenus.create({ id: 'verstak-capture-selection', title: 'Send selection to Verstak', contexts: ['selection'] });
|
||||
ext.contextMenus.create({ id: 'verstak-capture-link', title: 'Send link to Verstak', contexts: ['link'] });
|
||||
});
|
||||
}
|
||||
|
||||
ext.runtime.onInstalled.addListener(setupContextMenus);
|
||||
if (ext.contextMenus && ext.contextMenus.onClicked) {
|
||||
ext.contextMenus.onClicked.addListener(function (info, tab) {
|
||||
var kind = info.menuItemId === 'verstak-capture-selection' ? 'selection'
|
||||
: info.menuItemId === 'verstak-capture-link' ? 'link'
|
||||
: 'page';
|
||||
sendOrQueue(captureFromInfo(kind, info, tab || {}));
|
||||
});
|
||||
}
|
||||
|
||||
ext.runtime.onMessage.addListener(function (message) {
|
||||
if (!message || message.type !== 'verstak.capture') return undefined;
|
||||
if (message.action === 'retryPending') {
|
||||
return retryPending();
|
||||
}
|
||||
return activeTab().then(function (tab) {
|
||||
return sendOrQueue(captureFromInfo(message.kind || 'page', message, tab));
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
body {
|
||||
min-width: 220px;
|
||||
margin: 0;
|
||||
font: 13px system-ui, sans-serif;
|
||||
background: #111827;
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
main {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
button {
|
||||
border: 1px solid #374151;
|
||||
border-radius: 4px;
|
||||
padding: 8px 10px;
|
||||
background: #1f2937;
|
||||
color: #f9fafb;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
border-color: #10b981;
|
||||
}
|
||||
|
||||
#status {
|
||||
min-height: 18px;
|
||||
margin: 0;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="popup.css">
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<button id="capture-page">Send Page</button>
|
||||
<button id="capture-selection">Send Selection</button>
|
||||
<button id="retry">Retry Pending</button>
|
||||
<p id="status"></p>
|
||||
</main>
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
(function () {
|
||||
'use strict';
|
||||
|
||||
var ext = typeof browser !== 'undefined' ? browser : chrome;
|
||||
var statusEl = document.getElementById('status');
|
||||
|
||||
function setStatus(text) {
|
||||
statusEl.textContent = text;
|
||||
}
|
||||
|
||||
function send(message) {
|
||||
setStatus('Sending...');
|
||||
Promise.resolve(ext.runtime.sendMessage(message)).then(function (result) {
|
||||
if (result && result.status === 'queued') setStatus('Queued until Verstak is available');
|
||||
else setStatus('Sent');
|
||||
}).catch(function (err) {
|
||||
setStatus(err && err.message ? err.message : String(err));
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('capture-page').addEventListener('click', function () {
|
||||
send({ type: 'verstak.capture', kind: 'page' });
|
||||
});
|
||||
|
||||
document.getElementById('capture-selection').addEventListener('click', function () {
|
||||
send({ type: 'verstak.capture', kind: 'selection' });
|
||||
});
|
||||
|
||||
document.getElementById('retry').addEventListener('click', function () {
|
||||
send({ type: 'verstak.capture', action: 'retryPending' });
|
||||
});
|
||||
})();
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
(function (root) {
|
||||
'use strict';
|
||||
|
||||
var CAPTURE_SCHEMA_VERSION = 1;
|
||||
var DEFAULT_RECEIVER_URL = 'http://127.0.0.1:47731/api/browser-inbox/v1/captures';
|
||||
|
||||
function nowIso() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
function randomId() {
|
||||
var cryptoObj = root.crypto || (root.require && root.require('crypto'));
|
||||
if (cryptoObj && cryptoObj.randomUUID) return cryptoObj.randomUUID();
|
||||
return 'cap_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2);
|
||||
}
|
||||
|
||||
function cleanString(value, maxLength) {
|
||||
var text = String(value == null ? '' : value).replace(/\s+/g, ' ').trim();
|
||||
if (maxLength && text.length > maxLength) return text.slice(0, maxLength);
|
||||
return text;
|
||||
}
|
||||
|
||||
function hostname(url) {
|
||||
try {
|
||||
return new URL(url).hostname;
|
||||
} catch (_) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function buildCapture(input) {
|
||||
input = input || {};
|
||||
var kind = input.kind || 'page';
|
||||
var pageURL = cleanString(input.url || input.pageUrl || '', 4096);
|
||||
var payload = {
|
||||
schemaVersion: CAPTURE_SCHEMA_VERSION,
|
||||
captureId: input.captureId || randomId(),
|
||||
capturedAt: input.capturedAt || nowIso(),
|
||||
source: 'verstak-browser-extension',
|
||||
kind: kind,
|
||||
page: {
|
||||
url: pageURL,
|
||||
title: cleanString(input.title || '', 512),
|
||||
domain: hostname(pageURL)
|
||||
},
|
||||
browser: {
|
||||
name: cleanString(input.browserName || '', 64)
|
||||
}
|
||||
};
|
||||
|
||||
if (kind === 'selection') {
|
||||
payload.selection = {
|
||||
text: cleanString(input.selectionText || input.text || '', 20000)
|
||||
};
|
||||
}
|
||||
if (kind === 'link') {
|
||||
payload.link = {
|
||||
url: cleanString(input.linkUrl || '', 4096),
|
||||
text: cleanString(input.linkText || input.selectionText || '', 512)
|
||||
};
|
||||
}
|
||||
if (input.context) payload.context = input.context;
|
||||
return payload;
|
||||
}
|
||||
|
||||
function validateCapture(payload) {
|
||||
if (!payload || typeof payload !== 'object') throw new Error('payload must be an object');
|
||||
if (payload.schemaVersion !== CAPTURE_SCHEMA_VERSION) throw new Error('unsupported schemaVersion');
|
||||
if (!payload.captureId) throw new Error('captureId is required');
|
||||
if (!payload.capturedAt) throw new Error('capturedAt is required');
|
||||
if (['page', 'selection', 'link'].indexOf(payload.kind) === -1) throw new Error('unsupported kind');
|
||||
if (!payload.page || !payload.page.url) throw new Error('page.url is required');
|
||||
if (payload.kind === 'selection' && (!payload.selection || !payload.selection.text)) throw new Error('selection.text is required');
|
||||
if (payload.kind === 'link' && (!payload.link || !payload.link.url)) throw new Error('link.url is required');
|
||||
return true;
|
||||
}
|
||||
|
||||
var api = {
|
||||
CAPTURE_SCHEMA_VERSION: CAPTURE_SCHEMA_VERSION,
|
||||
DEFAULT_RECEIVER_URL: DEFAULT_RECEIVER_URL,
|
||||
buildCapture: buildCapture,
|
||||
validateCapture: validateCapture
|
||||
};
|
||||
|
||||
root.VerstakBrowser = Object.assign(root.VerstakBrowser || {}, api);
|
||||
if (typeof module !== 'undefined') module.exports = api;
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : this);
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
(function (root) {
|
||||
'use strict';
|
||||
|
||||
var QUEUE_KEY = 'verstak.pendingCaptures';
|
||||
|
||||
function createMemoryStorage(seed) {
|
||||
var state = Object.assign({}, seed || {});
|
||||
return {
|
||||
get: function (key) {
|
||||
return Promise.resolve(Object.prototype.hasOwnProperty.call(state, key) ? state[key] : undefined);
|
||||
},
|
||||
set: function (key, value) {
|
||||
state[key] = value;
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function browserStorageAdapter(browserApi) {
|
||||
var storage = browserApi && browserApi.storage && browserApi.storage.local;
|
||||
if (!storage) return createMemoryStorage();
|
||||
return {
|
||||
get: function (key) {
|
||||
return storage.get(key).then(function (result) { return result && result[key]; });
|
||||
},
|
||||
set: function (key, value) {
|
||||
var patch = {};
|
||||
patch[key] = value;
|
||||
return storage.set(patch);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function CaptureQueue(storage) {
|
||||
this.storage = storage || createMemoryStorage();
|
||||
}
|
||||
|
||||
CaptureQueue.prototype.list = function () {
|
||||
return this.storage.get(QUEUE_KEY).then(function (items) {
|
||||
return Array.isArray(items) ? items : [];
|
||||
});
|
||||
};
|
||||
|
||||
CaptureQueue.prototype.enqueue = function (payload) {
|
||||
var self = this;
|
||||
return this.list().then(function (items) {
|
||||
items.push(payload);
|
||||
return self.storage.set(QUEUE_KEY, items).then(function () { return items; });
|
||||
});
|
||||
};
|
||||
|
||||
CaptureQueue.prototype.replace = function (items) {
|
||||
return this.storage.set(QUEUE_KEY, Array.isArray(items) ? items : []);
|
||||
};
|
||||
|
||||
CaptureQueue.prototype.retry = function (sender) {
|
||||
var self = this;
|
||||
return this.list().then(function (items) {
|
||||
var sent = 0;
|
||||
var pending = [];
|
||||
return items.reduce(function (chain, item) {
|
||||
return chain.then(function () {
|
||||
return sender(item).then(function () {
|
||||
sent += 1;
|
||||
}).catch(function () {
|
||||
pending.push(item);
|
||||
});
|
||||
});
|
||||
}, Promise.resolve()).then(function () {
|
||||
return self.replace(pending).then(function () {
|
||||
return { sent: sent, pending: pending.length };
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var api = {
|
||||
QUEUE_KEY: QUEUE_KEY,
|
||||
CaptureQueue: CaptureQueue,
|
||||
browserStorageAdapter: browserStorageAdapter,
|
||||
createMemoryStorage: createMemoryStorage
|
||||
};
|
||||
root.VerstakBrowser = Object.assign(root.VerstakBrowser || {}, api);
|
||||
if (typeof module !== 'undefined') module.exports = api;
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : this);
|
||||
Loading…
Reference in New Issue