(function (root) { 'use strict'; var ACTIVITY_STATE_KEY = 'verstak.domainActivity.v1'; var MAX_CHECKPOINT_GAP_MS = 10 * 60 * 1000; var ACK_RETENTION_MS = 30 * 24 * 60 * 60 * 1000; var MAX_ACKNOWLEDGED_IDS = 1000; function createMemoryActivityStorage(seed) { var state = Object.assign({}, seed || {}); return { get: function (key) { return Promise.resolve(state[key]); }, set: function (key, value) { state[key] = value; return Promise.resolve(); } }; } function browserActivityStorageAdapter(browserApi) { var storage = browserApi && browserApi.storage && browserApi.storage.local; if (!storage) return createMemoryActivityStorage(); 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 clone(value) { return JSON.parse(JSON.stringify(value)); } function isFiniteNumber(value) { return typeof value === 'number' && Number.isFinite(value); } function initialState() { return { activeAccumulator: {}, pendingBatches: [], acknowledgedIds: [] }; } function normalizedState(value) { var state = value && typeof value === 'object' ? value : {}; return { activeAccumulator: state.activeAccumulator && typeof state.activeAccumulator === 'object' ? state.activeAccumulator : {}, pendingBatches: Array.isArray(state.pendingBatches) ? state.pendingBatches : [], acknowledgedIds: Array.isArray(state.acknowledgedIds) ? state.acknowledgedIds : [] }; } function batchId(now) { var cryptoObj = root.crypto; if (cryptoObj && typeof cryptoObj.randomUUID === 'function') return cryptoObj.randomUUID(); return 'activity-' + String(now) + '-' + Math.random().toString(36).slice(2); } function toISOString(time) { return new Date(time).toISOString(); } function isExcludedHostname(hostname, excludedDomains) { var normalizer = root.VerstakBrowser && root.VerstakBrowser.normalizeHostnameV1; var canonicalHostname = typeof normalizer === 'function' ? normalizer(hostname) : ''; if (!canonicalHostname) return true; return (Array.isArray(excludedDomains) ? excludedDomains : []).some(function (value) { var excluded = typeof normalizer === 'function' ? normalizer(value) : ''; return excluded && (canonicalHostname === excluded || canonicalHostname.slice(-(excluded.length + 1)) === '.' + excluded); }); } function DomainActivityTracker(storage, sender, options) { options = options || {}; this.storage = storage || createMemoryActivityStorage(); this.sender = typeof sender === 'function' ? sender : function () { return Promise.reject(new Error('activity sender unavailable')); }; this.maxCheckpointGapMs = isFiniteNumber(options.maxCheckpointGapMs) ? options.maxCheckpointGapMs : MAX_CHECKPOINT_GAP_MS; this.now = typeof options.now === 'function' ? options.now : function () { return Date.now(); }; this.state = initialState(); this.enabled = false; this.activeHostname = ''; this.serial = Promise.resolve(); } DomainActivityTracker.prototype.enqueue = function (operation) { var next = this.serial.then(operation, operation); this.serial = next.catch(function () {}); return next; }; DomainActivityTracker.prototype.initialize = function () { var self = this; return this.enqueue(function () { return self.storage.get(ACTIVITY_STATE_KEY).then(function (value) { self.state = normalizedState(value); self.pruneAcknowledged(self.now()); return self.persist().then(function () { return self.getStateUnsafe(); }); }); }); }; DomainActivityTracker.prototype.getState = function () { var self = this; return this.enqueue(function () { return self.getStateUnsafe(); }); }; DomainActivityTracker.prototype.getStateUnsafe = function () { return clone(this.state); }; DomainActivityTracker.prototype.persist = function () { return this.storage.set(ACTIVITY_STATE_KEY, clone(this.state)); }; DomainActivityTracker.prototype.setEnabled = function (enabled) { var self = this; return this.enqueue(function () { self.enabled = enabled === true; if (!self.enabled) { self.activeHostname = ''; self.state.activeAccumulator = {}; self.state.pendingBatches = []; } return self.persist().then(function () { return self.getStateUnsafe(); }); }); }; DomainActivityTracker.prototype.setActiveHostname = function (hostname, active, observedAt) { var self = this; var now = isFiniteNumber(observedAt) ? observedAt : this.now(); return this.enqueue(function () { if (!self.enabled || !active || !hostname) { self.checkpointUnsafe(now); self.activeHostname = ''; return self.persist().then(function () { return self.getStateUnsafe(); }); } self.checkpointUnsafe(now); self.activeHostname = hostname; self.ensureAccumulator(hostname, now); return self.persist().then(function () { return self.getStateUnsafe(); }); }); }; DomainActivityTracker.prototype.checkpoint = function (observedAt) { var self = this; var now = isFiniteNumber(observedAt) ? observedAt : this.now(); return this.enqueue(function () { self.checkpointUnsafe(now); return self.persist().then(function () { return self.getStateUnsafe(); }); }); }; DomainActivityTracker.prototype.ensureAccumulator = function (hostname, now) { if (!this.state.activeAccumulator[hostname]) { this.state.activeAccumulator[hostname] = { hostname: hostname, startedAt: now, lastCheckpointAt: now, durationMs: 0 }; } return this.state.activeAccumulator[hostname]; }; DomainActivityTracker.prototype.checkpointUnsafe = function (now) { if (!this.enabled || !this.activeHostname) return; var accumulator = this.ensureAccumulator(this.activeHostname, now); var previous = accumulator.lastCheckpointAt; var delta = now - previous; if (!isFiniteNumber(previous) || delta < 0 || delta > this.maxCheckpointGapMs) { accumulator.startedAt = now; accumulator.lastCheckpointAt = now; accumulator.durationMs = 0; return; } if (delta === 0) return; accumulator.durationMs += delta; accumulator.lastCheckpointAt = now; }; DomainActivityTracker.prototype.flush = function (observedAt) { var self = this; var now = isFiniteNumber(observedAt) ? observedAt : this.now(); return this.enqueue(function () { if (!self.enabled) return self.getStateUnsafe(); self.checkpointUnsafe(now); var entries = []; Object.keys(self.state.activeAccumulator).forEach(function (hostname) { var accumulator = self.state.activeAccumulator[hostname]; var durationSeconds = Math.floor(Number(accumulator.durationMs || 0) / 1000); if (durationSeconds < 1) return; entries.push({ hostname: accumulator.hostname, startedAt: toISOString(accumulator.startedAt), endedAt: toISOString(accumulator.lastCheckpointAt), durationSeconds: durationSeconds }); }); if (entries.length) { self.state.pendingBatches.push({ schemaVersion: 1, batchId: batchId(now), createdAt: toISOString(now), source: 'verstak-browser-extension', entries: entries }); self.state.activeAccumulator = {}; if (self.activeHostname) self.ensureAccumulator(self.activeHostname, now); } return self.persist().then(function () { return self.deliverPendingUnsafe(now); }); }); }; DomainActivityTracker.prototype.retryPending = function () { var self = this; return this.enqueue(function () { if (!self.enabled) return self.getStateUnsafe(); return self.deliverPendingUnsafe(self.now()); }); }; DomainActivityTracker.prototype.deliverPendingUnsafe = function (now) { var self = this; var pending = this.state.pendingBatches.slice(); return pending.reduce(function (chain, batch) { return chain.then(function () { if (!self.hasPendingBatch(batch.batchId)) return undefined; var immutableBatch = clone(batch); return self.sender(immutableBatch).then(function (result) { if (result && result.batchId && result.batchId !== batch.batchId) { throw new Error('receiver acknowledged a different activity batch'); } self.state.pendingBatches = self.state.pendingBatches.filter(function (item) { return item.batchId !== batch.batchId; }); self.state.acknowledgedIds.push({ id: batch.batchId, acknowledgedAt: toISOString(now) }); self.pruneAcknowledged(now); return self.persist(); }); }); }, Promise.resolve()).then(function () { return self.getStateUnsafe(); }).catch(function () { return self.persist().then(function () { return self.getStateUnsafe(); }); }); }; DomainActivityTracker.prototype.hasPendingBatch = function (batchID) { return this.state.pendingBatches.some(function (batch) { return batch.batchId === batchID; }); }; DomainActivityTracker.prototype.pruneAcknowledged = function (now) { var cutoff = now - ACK_RETENTION_MS; var seen = {}; this.state.acknowledgedIds = this.state.acknowledgedIds.filter(function (entry) { var timestamp = Date.parse(entry && entry.acknowledgedAt); if (!entry || !entry.id || !Number.isFinite(timestamp) || timestamp < cutoff || seen[entry.id]) return false; seen[entry.id] = true; return true; }).slice(-MAX_ACKNOWLEDGED_IDS); }; var api = { ACTIVITY_STATE_KEY: ACTIVITY_STATE_KEY, MAX_CHECKPOINT_GAP_MS: MAX_CHECKPOINT_GAP_MS, createMemoryActivityStorage: createMemoryActivityStorage, browserActivityStorageAdapter: browserActivityStorageAdapter, DomainActivityTracker: DomainActivityTracker, isExcludedHostname: isExcludedHostname }; root.VerstakBrowser = Object.assign(root.VerstakBrowser || {}, api); if (typeof module !== 'undefined') module.exports = api; })(typeof globalThis !== 'undefined' ? globalThis : this);