@tanstack/offline-transactions 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +219 -0
- package/dist/cjs/OfflineExecutor.cjs +266 -0
- package/dist/cjs/OfflineExecutor.cjs.map +1 -0
- package/dist/cjs/OfflineExecutor.d.cts +39 -0
- package/dist/cjs/api/OfflineAction.cjs +47 -0
- package/dist/cjs/api/OfflineAction.cjs.map +1 -0
- package/dist/cjs/api/OfflineAction.d.cts +3 -0
- package/dist/cjs/api/OfflineTransaction.cjs +96 -0
- package/dist/cjs/api/OfflineTransaction.cjs.map +1 -0
- package/dist/cjs/api/OfflineTransaction.d.cts +18 -0
- package/dist/cjs/connectivity/OnlineDetector.cjs +73 -0
- package/dist/cjs/connectivity/OnlineDetector.cjs.map +1 -0
- package/dist/cjs/connectivity/OnlineDetector.d.cts +15 -0
- package/dist/cjs/coordination/BroadcastChannelLeader.cjs +146 -0
- package/dist/cjs/coordination/BroadcastChannelLeader.cjs.map +1 -0
- package/dist/cjs/coordination/BroadcastChannelLeader.d.cts +26 -0
- package/dist/cjs/coordination/LeaderElection.cjs +31 -0
- package/dist/cjs/coordination/LeaderElection.cjs.map +1 -0
- package/dist/cjs/coordination/LeaderElection.d.cts +10 -0
- package/dist/cjs/coordination/WebLocksLeader.cjs +71 -0
- package/dist/cjs/coordination/WebLocksLeader.cjs.map +1 -0
- package/dist/cjs/coordination/WebLocksLeader.d.cts +10 -0
- package/dist/cjs/executor/KeyScheduler.cjs +106 -0
- package/dist/cjs/executor/KeyScheduler.cjs.map +1 -0
- package/dist/cjs/executor/KeyScheduler.d.cts +18 -0
- package/dist/cjs/executor/TransactionExecutor.cjs +236 -0
- package/dist/cjs/executor/TransactionExecutor.cjs.map +1 -0
- package/dist/cjs/executor/TransactionExecutor.d.cts +28 -0
- package/dist/cjs/index.cjs +34 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/cjs/index.d.cts +16 -0
- package/dist/cjs/outbox/OutboxManager.cjs +114 -0
- package/dist/cjs/outbox/OutboxManager.cjs.map +1 -0
- package/dist/cjs/outbox/OutboxManager.d.cts +18 -0
- package/dist/cjs/outbox/TransactionSerializer.cjs +135 -0
- package/dist/cjs/outbox/TransactionSerializer.cjs.map +1 -0
- package/dist/cjs/outbox/TransactionSerializer.d.cts +15 -0
- package/dist/cjs/retry/BackoffCalculator.cjs +14 -0
- package/dist/cjs/retry/BackoffCalculator.cjs.map +1 -0
- package/dist/cjs/retry/BackoffCalculator.d.cts +5 -0
- package/dist/cjs/retry/NonRetriableError.d.cts +1 -0
- package/dist/cjs/retry/RetryPolicy.cjs +33 -0
- package/dist/cjs/retry/RetryPolicy.cjs.map +1 -0
- package/dist/cjs/retry/RetryPolicy.d.cts +8 -0
- package/dist/cjs/storage/IndexedDBAdapter.cjs +104 -0
- package/dist/cjs/storage/IndexedDBAdapter.cjs.map +1 -0
- package/dist/cjs/storage/IndexedDBAdapter.d.cts +14 -0
- package/dist/cjs/storage/LocalStorageAdapter.cjs +71 -0
- package/dist/cjs/storage/LocalStorageAdapter.cjs.map +1 -0
- package/dist/cjs/storage/LocalStorageAdapter.d.cts +11 -0
- package/dist/cjs/storage/StorageAdapter.cjs +6 -0
- package/dist/cjs/storage/StorageAdapter.cjs.map +1 -0
- package/dist/cjs/storage/StorageAdapter.d.cts +9 -0
- package/dist/cjs/telemetry/tracer.cjs +91 -0
- package/dist/cjs/telemetry/tracer.cjs.map +1 -0
- package/dist/cjs/telemetry/tracer.d.cts +29 -0
- package/dist/cjs/types.cjs +10 -0
- package/dist/cjs/types.cjs.map +1 -0
- package/dist/cjs/types.d.cts +101 -0
- package/dist/esm/OfflineExecutor.d.ts +39 -0
- package/dist/esm/OfflineExecutor.js +266 -0
- package/dist/esm/OfflineExecutor.js.map +1 -0
- package/dist/esm/api/OfflineAction.d.ts +3 -0
- package/dist/esm/api/OfflineAction.js +47 -0
- package/dist/esm/api/OfflineAction.js.map +1 -0
- package/dist/esm/api/OfflineTransaction.d.ts +18 -0
- package/dist/esm/api/OfflineTransaction.js +96 -0
- package/dist/esm/api/OfflineTransaction.js.map +1 -0
- package/dist/esm/connectivity/OnlineDetector.d.ts +15 -0
- package/dist/esm/connectivity/OnlineDetector.js +73 -0
- package/dist/esm/connectivity/OnlineDetector.js.map +1 -0
- package/dist/esm/coordination/BroadcastChannelLeader.d.ts +26 -0
- package/dist/esm/coordination/BroadcastChannelLeader.js +146 -0
- package/dist/esm/coordination/BroadcastChannelLeader.js.map +1 -0
- package/dist/esm/coordination/LeaderElection.d.ts +10 -0
- package/dist/esm/coordination/LeaderElection.js +31 -0
- package/dist/esm/coordination/LeaderElection.js.map +1 -0
- package/dist/esm/coordination/WebLocksLeader.d.ts +10 -0
- package/dist/esm/coordination/WebLocksLeader.js +71 -0
- package/dist/esm/coordination/WebLocksLeader.js.map +1 -0
- package/dist/esm/executor/KeyScheduler.d.ts +18 -0
- package/dist/esm/executor/KeyScheduler.js +106 -0
- package/dist/esm/executor/KeyScheduler.js.map +1 -0
- package/dist/esm/executor/TransactionExecutor.d.ts +28 -0
- package/dist/esm/executor/TransactionExecutor.js +236 -0
- package/dist/esm/executor/TransactionExecutor.js.map +1 -0
- package/dist/esm/index.d.ts +16 -0
- package/dist/esm/index.js +34 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/outbox/OutboxManager.d.ts +18 -0
- package/dist/esm/outbox/OutboxManager.js +114 -0
- package/dist/esm/outbox/OutboxManager.js.map +1 -0
- package/dist/esm/outbox/TransactionSerializer.d.ts +15 -0
- package/dist/esm/outbox/TransactionSerializer.js +135 -0
- package/dist/esm/outbox/TransactionSerializer.js.map +1 -0
- package/dist/esm/retry/BackoffCalculator.d.ts +5 -0
- package/dist/esm/retry/BackoffCalculator.js +14 -0
- package/dist/esm/retry/BackoffCalculator.js.map +1 -0
- package/dist/esm/retry/NonRetriableError.d.ts +1 -0
- package/dist/esm/retry/RetryPolicy.d.ts +8 -0
- package/dist/esm/retry/RetryPolicy.js +33 -0
- package/dist/esm/retry/RetryPolicy.js.map +1 -0
- package/dist/esm/storage/IndexedDBAdapter.d.ts +14 -0
- package/dist/esm/storage/IndexedDBAdapter.js +104 -0
- package/dist/esm/storage/IndexedDBAdapter.js.map +1 -0
- package/dist/esm/storage/LocalStorageAdapter.d.ts +11 -0
- package/dist/esm/storage/LocalStorageAdapter.js +71 -0
- package/dist/esm/storage/LocalStorageAdapter.js.map +1 -0
- package/dist/esm/storage/StorageAdapter.d.ts +9 -0
- package/dist/esm/storage/StorageAdapter.js +6 -0
- package/dist/esm/storage/StorageAdapter.js.map +1 -0
- package/dist/esm/telemetry/tracer.d.ts +29 -0
- package/dist/esm/telemetry/tracer.js +91 -0
- package/dist/esm/telemetry/tracer.js.map +1 -0
- package/dist/esm/types.d.ts +101 -0
- package/dist/esm/types.js +10 -0
- package/dist/esm/types.js.map +1 -0
- package/package.json +66 -0
- package/src/OfflineExecutor.ts +360 -0
- package/src/api/OfflineAction.ts +68 -0
- package/src/api/OfflineTransaction.ts +134 -0
- package/src/connectivity/OnlineDetector.ts +87 -0
- package/src/coordination/BroadcastChannelLeader.ts +181 -0
- package/src/coordination/LeaderElection.ts +35 -0
- package/src/coordination/WebLocksLeader.ts +82 -0
- package/src/executor/KeyScheduler.ts +123 -0
- package/src/executor/TransactionExecutor.ts +330 -0
- package/src/index.ts +47 -0
- package/src/outbox/OutboxManager.ts +141 -0
- package/src/outbox/TransactionSerializer.ts +163 -0
- package/src/retry/BackoffCalculator.ts +13 -0
- package/src/retry/NonRetriableError.ts +1 -0
- package/src/retry/RetryPolicy.ts +41 -0
- package/src/storage/IndexedDBAdapter.ts +119 -0
- package/src/storage/LocalStorageAdapter.ts +79 -0
- package/src/storage/StorageAdapter.ts +11 -0
- package/src/telemetry/tracer.ts +156 -0
- package/src/types.ts +133 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const api = require("@opentelemetry/api");
|
|
4
|
+
const db = require("@tanstack/db");
|
|
5
|
+
const types = require("../types.cjs");
|
|
6
|
+
class OfflineTransaction {
|
|
7
|
+
// Will be typed properly - reference to OfflineExecutor
|
|
8
|
+
constructor(options, mutationFn, persistTransaction, executor) {
|
|
9
|
+
this.transaction = null;
|
|
10
|
+
this.offlineId = crypto.randomUUID();
|
|
11
|
+
this.mutationFnName = options.mutationFnName;
|
|
12
|
+
this.autoCommit = options.autoCommit ?? true;
|
|
13
|
+
this.idempotencyKey = options.idempotencyKey ?? crypto.randomUUID();
|
|
14
|
+
this.metadata = options.metadata ?? {};
|
|
15
|
+
this.persistTransaction = persistTransaction;
|
|
16
|
+
this.executor = executor;
|
|
17
|
+
}
|
|
18
|
+
mutate(callback) {
|
|
19
|
+
this.transaction = db.createTransaction({
|
|
20
|
+
id: this.offlineId,
|
|
21
|
+
autoCommit: false,
|
|
22
|
+
mutationFn: async () => {
|
|
23
|
+
const activeSpan = api.trace.getSpan(api.context.active());
|
|
24
|
+
const spanContext = activeSpan?.spanContext();
|
|
25
|
+
const offlineTransaction = {
|
|
26
|
+
id: this.offlineId,
|
|
27
|
+
mutationFnName: this.mutationFnName,
|
|
28
|
+
mutations: this.transaction.mutations,
|
|
29
|
+
keys: this.extractKeys(this.transaction.mutations),
|
|
30
|
+
idempotencyKey: this.idempotencyKey,
|
|
31
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
32
|
+
retryCount: 0,
|
|
33
|
+
nextAttemptAt: Date.now(),
|
|
34
|
+
metadata: this.metadata,
|
|
35
|
+
spanContext: spanContext ? {
|
|
36
|
+
traceId: spanContext.traceId,
|
|
37
|
+
spanId: spanContext.spanId,
|
|
38
|
+
traceFlags: spanContext.traceFlags,
|
|
39
|
+
traceState: spanContext.traceState?.serialize()
|
|
40
|
+
} : void 0,
|
|
41
|
+
version: 1
|
|
42
|
+
};
|
|
43
|
+
const completionPromise = this.executor.waitForTransactionCompletion(
|
|
44
|
+
this.offlineId
|
|
45
|
+
);
|
|
46
|
+
try {
|
|
47
|
+
await this.persistTransaction(offlineTransaction);
|
|
48
|
+
await completionPromise;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
const normalizedError = error instanceof Error ? error : new Error(String(error));
|
|
51
|
+
this.executor.rejectTransaction(this.offlineId, normalizedError);
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
},
|
|
56
|
+
metadata: this.metadata
|
|
57
|
+
});
|
|
58
|
+
this.transaction.mutate(() => {
|
|
59
|
+
callback();
|
|
60
|
+
});
|
|
61
|
+
if (this.autoCommit) {
|
|
62
|
+
this.commit().catch((error) => {
|
|
63
|
+
console.error(`Auto-commit failed:`, error);
|
|
64
|
+
throw error;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return this.transaction;
|
|
68
|
+
}
|
|
69
|
+
async commit() {
|
|
70
|
+
if (!this.transaction) {
|
|
71
|
+
throw new Error(`No mutations to commit. Call mutate() first.`);
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
await this.transaction.commit();
|
|
75
|
+
return this.transaction;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error instanceof types.NonRetriableError) {
|
|
78
|
+
this.transaction.rollback();
|
|
79
|
+
}
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
rollback() {
|
|
84
|
+
if (this.transaction) {
|
|
85
|
+
this.transaction.rollback();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
extractKeys(mutations) {
|
|
89
|
+
return mutations.map((mutation) => mutation.globalKey);
|
|
90
|
+
}
|
|
91
|
+
get id() {
|
|
92
|
+
return this.offlineId;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
exports.OfflineTransaction = OfflineTransaction;
|
|
96
|
+
//# sourceMappingURL=OfflineTransaction.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OfflineTransaction.cjs","sources":["../../../src/api/OfflineTransaction.ts"],"sourcesContent":["import { context, trace } from \"@opentelemetry/api\"\nimport { createTransaction } from \"@tanstack/db\"\nimport { NonRetriableError } from \"../types\"\nimport type { PendingMutation, Transaction } from \"@tanstack/db\"\nimport type {\n CreateOfflineTransactionOptions,\n OfflineMutationFn,\n OfflineTransaction as OfflineTransactionType,\n} from \"../types\"\n\nexport class OfflineTransaction {\n private offlineId: string\n private mutationFnName: string\n private autoCommit: boolean\n private idempotencyKey: string\n private metadata: Record<string, any>\n private transaction: Transaction | null = null\n private persistTransaction: (tx: OfflineTransactionType) => Promise<void>\n private executor: any // Will be typed properly - reference to OfflineExecutor\n\n constructor(\n options: CreateOfflineTransactionOptions,\n mutationFn: OfflineMutationFn,\n persistTransaction: (tx: OfflineTransactionType) => Promise<void>,\n executor: any\n ) {\n this.offlineId = crypto.randomUUID()\n this.mutationFnName = options.mutationFnName\n this.autoCommit = options.autoCommit ?? true\n this.idempotencyKey = options.idempotencyKey ?? crypto.randomUUID()\n this.metadata = options.metadata ?? {}\n this.persistTransaction = persistTransaction\n this.executor = executor\n }\n\n mutate(callback: () => void): Transaction {\n this.transaction = createTransaction({\n id: this.offlineId,\n autoCommit: false,\n mutationFn: async () => {\n // This is the blocking mutationFn that waits for the executor\n // First persist the transaction to the outbox\n const activeSpan = trace.getSpan(context.active())\n const spanContext = activeSpan?.spanContext()\n\n const offlineTransaction: OfflineTransactionType = {\n id: this.offlineId,\n mutationFnName: this.mutationFnName,\n mutations: this.transaction!.mutations,\n keys: this.extractKeys(this.transaction!.mutations),\n idempotencyKey: this.idempotencyKey,\n createdAt: new Date(),\n retryCount: 0,\n nextAttemptAt: Date.now(),\n metadata: this.metadata,\n spanContext: spanContext\n ? {\n traceId: spanContext.traceId,\n spanId: spanContext.spanId,\n traceFlags: spanContext.traceFlags,\n traceState: spanContext.traceState?.serialize(),\n }\n : undefined,\n version: 1,\n }\n\n const completionPromise = this.executor.waitForTransactionCompletion(\n this.offlineId\n )\n\n try {\n await this.persistTransaction(offlineTransaction)\n // Now block and wait for the executor to complete the real mutation\n await completionPromise\n } catch (error) {\n const normalizedError =\n error instanceof Error ? error : new Error(String(error))\n this.executor.rejectTransaction(this.offlineId, normalizedError)\n throw error\n }\n\n return\n },\n metadata: this.metadata,\n })\n\n this.transaction.mutate(() => {\n callback()\n })\n\n if (this.autoCommit) {\n // Auto-commit for direct OfflineTransaction usage\n this.commit().catch((error) => {\n console.error(`Auto-commit failed:`, error)\n throw error\n })\n }\n\n return this.transaction\n }\n\n async commit(): Promise<Transaction> {\n if (!this.transaction) {\n throw new Error(`No mutations to commit. Call mutate() first.`)\n }\n\n try {\n // Commit the TanStack DB transaction\n // This will trigger the mutationFn which handles persistence and waiting\n await this.transaction.commit()\n return this.transaction\n } catch (error) {\n // Only rollback for NonRetriableError - other errors should allow retry\n if (error instanceof NonRetriableError) {\n this.transaction.rollback()\n }\n throw error\n }\n }\n\n rollback(): void {\n if (this.transaction) {\n this.transaction.rollback()\n }\n }\n\n private extractKeys(mutations: Array<PendingMutation>): Array<string> {\n return mutations.map((mutation) => mutation.globalKey)\n }\n\n get id(): string {\n return this.offlineId\n }\n}\n"],"names":["createTransaction","trace","context","NonRetriableError"],"mappings":";;;;;AAUO,MAAM,mBAAmB;AAAA;AAAA,EAU9B,YACE,SACA,YACA,oBACA,UACA;AATF,SAAQ,cAAkC;AAUxC,SAAK,YAAY,OAAO,WAAA;AACxB,SAAK,iBAAiB,QAAQ;AAC9B,SAAK,aAAa,QAAQ,cAAc;AACxC,SAAK,iBAAiB,QAAQ,kBAAkB,OAAO,WAAA;AACvD,SAAK,WAAW,QAAQ,YAAY,CAAA;AACpC,SAAK,qBAAqB;AAC1B,SAAK,WAAW;AAAA,EAClB;AAAA,EAEA,OAAO,UAAmC;AACxC,SAAK,cAAcA,qBAAkB;AAAA,MACnC,IAAI,KAAK;AAAA,MACT,YAAY;AAAA,MACZ,YAAY,YAAY;AAGtB,cAAM,aAAaC,IAAAA,MAAM,QAAQC,IAAAA,QAAQ,QAAQ;AACjD,cAAM,cAAc,YAAY,YAAA;AAEhC,cAAM,qBAA6C;AAAA,UACjD,IAAI,KAAK;AAAA,UACT,gBAAgB,KAAK;AAAA,UACrB,WAAW,KAAK,YAAa;AAAA,UAC7B,MAAM,KAAK,YAAY,KAAK,YAAa,SAAS;AAAA,UAClD,gBAAgB,KAAK;AAAA,UACrB,+BAAe,KAAA;AAAA,UACf,YAAY;AAAA,UACZ,eAAe,KAAK,IAAA;AAAA,UACpB,UAAU,KAAK;AAAA,UACf,aAAa,cACT;AAAA,YACE,SAAS,YAAY;AAAA,YACrB,QAAQ,YAAY;AAAA,YACpB,YAAY,YAAY;AAAA,YACxB,YAAY,YAAY,YAAY,UAAA;AAAA,UAAU,IAEhD;AAAA,UACJ,SAAS;AAAA,QAAA;AAGX,cAAM,oBAAoB,KAAK,SAAS;AAAA,UACtC,KAAK;AAAA,QAAA;AAGP,YAAI;AACF,gBAAM,KAAK,mBAAmB,kBAAkB;AAEhD,gBAAM;AAAA,QACR,SAAS,OAAO;AACd,gBAAM,kBACJ,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;AAC1D,eAAK,SAAS,kBAAkB,KAAK,WAAW,eAAe;AAC/D,gBAAM;AAAA,QACR;AAEA;AAAA,MACF;AAAA,MACA,UAAU,KAAK;AAAA,IAAA,CAChB;AAED,SAAK,YAAY,OAAO,MAAM;AAC5B,eAAA;AAAA,IACF,CAAC;AAED,QAAI,KAAK,YAAY;AAEnB,WAAK,OAAA,EAAS,MAAM,CAAC,UAAU;AAC7B,gBAAQ,MAAM,uBAAuB,KAAK;AAC1C,cAAM;AAAA,MACR,CAAC;AAAA,IACH;AAEA,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,MAAM,SAA+B;AACnC,QAAI,CAAC,KAAK,aAAa;AACrB,YAAM,IAAI,MAAM,8CAA8C;AAAA,IAChE;AAEA,QAAI;AAGF,YAAM,KAAK,YAAY,OAAA;AACvB,aAAO,KAAK;AAAA,IACd,SAAS,OAAO;AAEd,UAAI,iBAAiBC,MAAAA,mBAAmB;AACtC,aAAK,YAAY,SAAA;AAAA,MACnB;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,WAAiB;AACf,QAAI,KAAK,aAAa;AACpB,WAAK,YAAY,SAAA;AAAA,IACnB;AAAA,EACF;AAAA,EAEQ,YAAY,WAAkD;AACpE,WAAO,UAAU,IAAI,CAAC,aAAa,SAAS,SAAS;AAAA,EACvD;AAAA,EAEA,IAAI,KAAa;AACf,WAAO,KAAK;AAAA,EACd;AACF;;"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Transaction } from '@tanstack/db';
|
|
2
|
+
import { CreateOfflineTransactionOptions, OfflineMutationFn, OfflineTransaction as OfflineTransactionType } from '../types.cjs';
|
|
3
|
+
export declare class OfflineTransaction {
|
|
4
|
+
private offlineId;
|
|
5
|
+
private mutationFnName;
|
|
6
|
+
private autoCommit;
|
|
7
|
+
private idempotencyKey;
|
|
8
|
+
private metadata;
|
|
9
|
+
private transaction;
|
|
10
|
+
private persistTransaction;
|
|
11
|
+
private executor;
|
|
12
|
+
constructor(options: CreateOfflineTransactionOptions, mutationFn: OfflineMutationFn, persistTransaction: (tx: OfflineTransactionType) => Promise<void>, executor: any);
|
|
13
|
+
mutate(callback: () => void): Transaction;
|
|
14
|
+
commit(): Promise<Transaction>;
|
|
15
|
+
rollback(): void;
|
|
16
|
+
private extractKeys;
|
|
17
|
+
get id(): string;
|
|
18
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
class DefaultOnlineDetector {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
6
|
+
this.isListening = false;
|
|
7
|
+
this.handleOnline = () => {
|
|
8
|
+
this.notifyListeners();
|
|
9
|
+
};
|
|
10
|
+
this.handleVisibilityChange = () => {
|
|
11
|
+
if (document.visibilityState === `visible`) {
|
|
12
|
+
this.notifyListeners();
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
this.startListening();
|
|
16
|
+
}
|
|
17
|
+
startListening() {
|
|
18
|
+
if (this.isListening) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
this.isListening = true;
|
|
22
|
+
if (typeof window !== `undefined`) {
|
|
23
|
+
window.addEventListener(`online`, this.handleOnline);
|
|
24
|
+
document.addEventListener(`visibilitychange`, this.handleVisibilityChange);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
stopListening() {
|
|
28
|
+
if (!this.isListening) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
this.isListening = false;
|
|
32
|
+
if (typeof window !== `undefined`) {
|
|
33
|
+
window.removeEventListener(`online`, this.handleOnline);
|
|
34
|
+
document.removeEventListener(
|
|
35
|
+
`visibilitychange`,
|
|
36
|
+
this.handleVisibilityChange
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
notifyListeners() {
|
|
41
|
+
for (const listener of this.listeners) {
|
|
42
|
+
try {
|
|
43
|
+
listener();
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.warn(`OnlineDetector listener error:`, error);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
subscribe(callback) {
|
|
50
|
+
this.listeners.add(callback);
|
|
51
|
+
return () => {
|
|
52
|
+
this.listeners.delete(callback);
|
|
53
|
+
if (this.listeners.size === 0) {
|
|
54
|
+
this.stopListening();
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
notifyOnline() {
|
|
59
|
+
this.notifyListeners();
|
|
60
|
+
}
|
|
61
|
+
isOnline() {
|
|
62
|
+
if (typeof navigator !== `undefined`) {
|
|
63
|
+
return navigator.onLine;
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
dispose() {
|
|
68
|
+
this.stopListening();
|
|
69
|
+
this.listeners.clear();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
exports.DefaultOnlineDetector = DefaultOnlineDetector;
|
|
73
|
+
//# sourceMappingURL=OnlineDetector.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OnlineDetector.cjs","sources":["../../../src/connectivity/OnlineDetector.ts"],"sourcesContent":["import type { OnlineDetector } from \"../types\"\n\nexport class DefaultOnlineDetector implements OnlineDetector {\n private listeners: Set<() => void> = new Set()\n private isListening = false\n\n constructor() {\n this.startListening()\n }\n\n private startListening(): void {\n if (this.isListening) {\n return\n }\n\n this.isListening = true\n\n if (typeof window !== `undefined`) {\n window.addEventListener(`online`, this.handleOnline)\n document.addEventListener(`visibilitychange`, this.handleVisibilityChange)\n }\n }\n\n private stopListening(): void {\n if (!this.isListening) {\n return\n }\n\n this.isListening = false\n\n if (typeof window !== `undefined`) {\n window.removeEventListener(`online`, this.handleOnline)\n document.removeEventListener(\n `visibilitychange`,\n this.handleVisibilityChange\n )\n }\n }\n\n private handleOnline = (): void => {\n this.notifyListeners()\n }\n\n private handleVisibilityChange = (): void => {\n if (document.visibilityState === `visible`) {\n this.notifyListeners()\n }\n }\n\n private notifyListeners(): void {\n for (const listener of this.listeners) {\n try {\n listener()\n } catch (error) {\n console.warn(`OnlineDetector listener error:`, error)\n }\n }\n }\n\n subscribe(callback: () => void): () => void {\n this.listeners.add(callback)\n\n return () => {\n this.listeners.delete(callback)\n\n if (this.listeners.size === 0) {\n this.stopListening()\n }\n }\n }\n\n notifyOnline(): void {\n this.notifyListeners()\n }\n\n isOnline(): boolean {\n if (typeof navigator !== `undefined`) {\n return navigator.onLine\n }\n return true\n }\n\n dispose(): void {\n this.stopListening()\n this.listeners.clear()\n }\n}\n"],"names":[],"mappings":";;AAEO,MAAM,sBAAgD;AAAA,EAI3D,cAAc;AAHd,SAAQ,gCAAiC,IAAA;AACzC,SAAQ,cAAc;AAmCtB,SAAQ,eAAe,MAAY;AACjC,WAAK,gBAAA;AAAA,IACP;AAEA,SAAQ,yBAAyB,MAAY;AAC3C,UAAI,SAAS,oBAAoB,WAAW;AAC1C,aAAK,gBAAA;AAAA,MACP;AAAA,IACF;AAxCE,SAAK,eAAA;AAAA,EACP;AAAA,EAEQ,iBAAuB;AAC7B,QAAI,KAAK,aAAa;AACpB;AAAA,IACF;AAEA,SAAK,cAAc;AAEnB,QAAI,OAAO,WAAW,aAAa;AACjC,aAAO,iBAAiB,UAAU,KAAK,YAAY;AACnD,eAAS,iBAAiB,oBAAoB,KAAK,sBAAsB;AAAA,IAC3E;AAAA,EACF;AAAA,EAEQ,gBAAsB;AAC5B,QAAI,CAAC,KAAK,aAAa;AACrB;AAAA,IACF;AAEA,SAAK,cAAc;AAEnB,QAAI,OAAO,WAAW,aAAa;AACjC,aAAO,oBAAoB,UAAU,KAAK,YAAY;AACtD,eAAS;AAAA,QACP;AAAA,QACA,KAAK;AAAA,MAAA;AAAA,IAET;AAAA,EACF;AAAA,EAYQ,kBAAwB;AAC9B,eAAW,YAAY,KAAK,WAAW;AACrC,UAAI;AACF,iBAAA;AAAA,MACF,SAAS,OAAO;AACd,gBAAQ,KAAK,kCAAkC,KAAK;AAAA,MACtD;AAAA,IACF;AAAA,EACF;AAAA,EAEA,UAAU,UAAkC;AAC1C,SAAK,UAAU,IAAI,QAAQ;AAE3B,WAAO,MAAM;AACX,WAAK,UAAU,OAAO,QAAQ;AAE9B,UAAI,KAAK,UAAU,SAAS,GAAG;AAC7B,aAAK,cAAA;AAAA,MACP;AAAA,IACF;AAAA,EACF;AAAA,EAEA,eAAqB;AACnB,SAAK,gBAAA;AAAA,EACP;AAAA,EAEA,WAAoB;AAClB,QAAI,OAAO,cAAc,aAAa;AACpC,aAAO,UAAU;AAAA,IACnB;AACA,WAAO;AAAA,EACT;AAAA,EAEA,UAAgB;AACd,SAAK,cAAA;AACL,SAAK,UAAU,MAAA;AAAA,EACjB;AACF;;"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { OnlineDetector } from '../types.cjs';
|
|
2
|
+
export declare class DefaultOnlineDetector implements OnlineDetector {
|
|
3
|
+
private listeners;
|
|
4
|
+
private isListening;
|
|
5
|
+
constructor();
|
|
6
|
+
private startListening;
|
|
7
|
+
private stopListening;
|
|
8
|
+
private handleOnline;
|
|
9
|
+
private handleVisibilityChange;
|
|
10
|
+
private notifyListeners;
|
|
11
|
+
subscribe(callback: () => void): () => void;
|
|
12
|
+
notifyOnline(): void;
|
|
13
|
+
isOnline(): boolean;
|
|
14
|
+
dispose(): void;
|
|
15
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const LeaderElection = require("./LeaderElection.cjs");
|
|
4
|
+
class BroadcastChannelLeader extends LeaderElection.BaseLeaderElection {
|
|
5
|
+
constructor(channelName = `offline-executor-leader`) {
|
|
6
|
+
super();
|
|
7
|
+
this.channel = null;
|
|
8
|
+
this.heartbeatInterval = null;
|
|
9
|
+
this.electionTimeout = null;
|
|
10
|
+
this.lastLeaderHeartbeat = 0;
|
|
11
|
+
this.heartbeatIntervalMs = 5e3;
|
|
12
|
+
this.electionTimeoutMs = 1e4;
|
|
13
|
+
this.handleMessage = (event) => {
|
|
14
|
+
const { type, tabId, timestamp } = event.data;
|
|
15
|
+
if (tabId === this.tabId) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
switch (type) {
|
|
19
|
+
case `heartbeat`:
|
|
20
|
+
if (this.isLeaderState && tabId < this.tabId) {
|
|
21
|
+
this.releaseLeadership();
|
|
22
|
+
} else if (!this.isLeaderState) {
|
|
23
|
+
this.lastLeaderHeartbeat = timestamp;
|
|
24
|
+
this.cancelElection();
|
|
25
|
+
}
|
|
26
|
+
break;
|
|
27
|
+
case `election`:
|
|
28
|
+
if (this.isLeaderState) {
|
|
29
|
+
this.sendHeartbeat();
|
|
30
|
+
} else if (tabId > this.tabId) {
|
|
31
|
+
this.startElection();
|
|
32
|
+
}
|
|
33
|
+
break;
|
|
34
|
+
case `leadership-claim`:
|
|
35
|
+
if (this.isLeaderState && tabId < this.tabId) {
|
|
36
|
+
this.releaseLeadership();
|
|
37
|
+
}
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
this.channelName = channelName;
|
|
42
|
+
this.tabId = crypto.randomUUID();
|
|
43
|
+
this.setupChannel();
|
|
44
|
+
}
|
|
45
|
+
setupChannel() {
|
|
46
|
+
if (!this.isBroadcastChannelSupported()) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
this.channel = new BroadcastChannel(this.channelName);
|
|
50
|
+
this.channel.addEventListener(`message`, this.handleMessage);
|
|
51
|
+
}
|
|
52
|
+
async requestLeadership() {
|
|
53
|
+
if (!this.isBroadcastChannelSupported()) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
if (this.isLeaderState) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
this.startElection();
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
setTimeout(() => {
|
|
62
|
+
resolve(this.isLeaderState);
|
|
63
|
+
}, 1e3);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
startElection() {
|
|
67
|
+
if (this.electionTimeout) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
this.sendMessage({
|
|
71
|
+
type: `election`,
|
|
72
|
+
tabId: this.tabId,
|
|
73
|
+
timestamp: Date.now()
|
|
74
|
+
});
|
|
75
|
+
this.electionTimeout = window.setTimeout(() => {
|
|
76
|
+
const timeSinceLastHeartbeat = Date.now() - this.lastLeaderHeartbeat;
|
|
77
|
+
if (timeSinceLastHeartbeat > this.electionTimeoutMs) {
|
|
78
|
+
this.claimLeadership();
|
|
79
|
+
}
|
|
80
|
+
this.electionTimeout = null;
|
|
81
|
+
}, this.electionTimeoutMs);
|
|
82
|
+
}
|
|
83
|
+
cancelElection() {
|
|
84
|
+
if (this.electionTimeout) {
|
|
85
|
+
clearTimeout(this.electionTimeout);
|
|
86
|
+
this.electionTimeout = null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
claimLeadership() {
|
|
90
|
+
this.notifyLeadershipChange(true);
|
|
91
|
+
this.sendMessage({
|
|
92
|
+
type: `leadership-claim`,
|
|
93
|
+
tabId: this.tabId,
|
|
94
|
+
timestamp: Date.now()
|
|
95
|
+
});
|
|
96
|
+
this.startHeartbeat();
|
|
97
|
+
}
|
|
98
|
+
startHeartbeat() {
|
|
99
|
+
if (this.heartbeatInterval) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
this.sendHeartbeat();
|
|
103
|
+
this.heartbeatInterval = window.setInterval(() => {
|
|
104
|
+
this.sendHeartbeat();
|
|
105
|
+
}, this.heartbeatIntervalMs);
|
|
106
|
+
}
|
|
107
|
+
stopHeartbeat() {
|
|
108
|
+
if (this.heartbeatInterval) {
|
|
109
|
+
clearInterval(this.heartbeatInterval);
|
|
110
|
+
this.heartbeatInterval = null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
sendHeartbeat() {
|
|
114
|
+
this.sendMessage({
|
|
115
|
+
type: `heartbeat`,
|
|
116
|
+
tabId: this.tabId,
|
|
117
|
+
timestamp: Date.now()
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
sendMessage(message) {
|
|
121
|
+
if (this.channel) {
|
|
122
|
+
this.channel.postMessage(message);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
releaseLeadership() {
|
|
126
|
+
this.stopHeartbeat();
|
|
127
|
+
this.cancelElection();
|
|
128
|
+
this.notifyLeadershipChange(false);
|
|
129
|
+
}
|
|
130
|
+
isBroadcastChannelSupported() {
|
|
131
|
+
return typeof BroadcastChannel !== `undefined`;
|
|
132
|
+
}
|
|
133
|
+
static isSupported() {
|
|
134
|
+
return typeof BroadcastChannel !== `undefined`;
|
|
135
|
+
}
|
|
136
|
+
dispose() {
|
|
137
|
+
this.releaseLeadership();
|
|
138
|
+
if (this.channel) {
|
|
139
|
+
this.channel.removeEventListener(`message`, this.handleMessage);
|
|
140
|
+
this.channel.close();
|
|
141
|
+
this.channel = null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
exports.BroadcastChannelLeader = BroadcastChannelLeader;
|
|
146
|
+
//# sourceMappingURL=BroadcastChannelLeader.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BroadcastChannelLeader.cjs","sources":["../../../src/coordination/BroadcastChannelLeader.ts"],"sourcesContent":["import { BaseLeaderElection } from \"./LeaderElection\"\n\ninterface LeaderMessage {\n type: `heartbeat` | `election` | `leadership-claim`\n tabId: string\n timestamp: number\n}\n\nexport class BroadcastChannelLeader extends BaseLeaderElection {\n private channelName: string\n private tabId: string\n private channel: BroadcastChannel | null = null\n private heartbeatInterval: number | null = null\n private electionTimeout: number | null = null\n private lastLeaderHeartbeat = 0\n private readonly heartbeatIntervalMs = 5000\n private readonly electionTimeoutMs = 10000\n\n constructor(channelName = `offline-executor-leader`) {\n super()\n this.channelName = channelName\n this.tabId = crypto.randomUUID()\n this.setupChannel()\n }\n\n private setupChannel(): void {\n if (!this.isBroadcastChannelSupported()) {\n return\n }\n\n this.channel = new BroadcastChannel(this.channelName)\n this.channel.addEventListener(`message`, this.handleMessage)\n }\n\n private handleMessage = (event: MessageEvent<LeaderMessage>): void => {\n const { type, tabId, timestamp } = event.data\n\n if (tabId === this.tabId) {\n return\n }\n\n switch (type) {\n case `heartbeat`:\n if (this.isLeaderState && tabId < this.tabId) {\n this.releaseLeadership()\n } else if (!this.isLeaderState) {\n this.lastLeaderHeartbeat = timestamp\n this.cancelElection()\n }\n break\n\n case `election`:\n if (this.isLeaderState) {\n this.sendHeartbeat()\n } else if (tabId > this.tabId) {\n this.startElection()\n }\n break\n\n case `leadership-claim`:\n if (this.isLeaderState && tabId < this.tabId) {\n this.releaseLeadership()\n }\n break\n }\n }\n\n async requestLeadership(): Promise<boolean> {\n if (!this.isBroadcastChannelSupported()) {\n return false\n }\n\n if (this.isLeaderState) {\n return true\n }\n\n this.startElection()\n\n return new Promise((resolve) => {\n setTimeout(() => {\n resolve(this.isLeaderState)\n }, 1000)\n })\n }\n\n private startElection(): void {\n if (this.electionTimeout) {\n return\n }\n\n this.sendMessage({\n type: `election`,\n tabId: this.tabId,\n timestamp: Date.now(),\n })\n\n this.electionTimeout = window.setTimeout(() => {\n const timeSinceLastHeartbeat = Date.now() - this.lastLeaderHeartbeat\n\n if (timeSinceLastHeartbeat > this.electionTimeoutMs) {\n this.claimLeadership()\n }\n\n this.electionTimeout = null\n }, this.electionTimeoutMs)\n }\n\n private cancelElection(): void {\n if (this.electionTimeout) {\n clearTimeout(this.electionTimeout)\n this.electionTimeout = null\n }\n }\n\n private claimLeadership(): void {\n this.notifyLeadershipChange(true)\n this.sendMessage({\n type: `leadership-claim`,\n tabId: this.tabId,\n timestamp: Date.now(),\n })\n this.startHeartbeat()\n }\n\n private startHeartbeat(): void {\n if (this.heartbeatInterval) {\n return\n }\n\n this.sendHeartbeat()\n\n this.heartbeatInterval = window.setInterval(() => {\n this.sendHeartbeat()\n }, this.heartbeatIntervalMs)\n }\n\n private stopHeartbeat(): void {\n if (this.heartbeatInterval) {\n clearInterval(this.heartbeatInterval)\n this.heartbeatInterval = null\n }\n }\n\n private sendHeartbeat(): void {\n this.sendMessage({\n type: `heartbeat`,\n tabId: this.tabId,\n timestamp: Date.now(),\n })\n }\n\n private sendMessage(message: LeaderMessage): void {\n if (this.channel) {\n this.channel.postMessage(message)\n }\n }\n\n releaseLeadership(): void {\n this.stopHeartbeat()\n this.cancelElection()\n this.notifyLeadershipChange(false)\n }\n\n private isBroadcastChannelSupported(): boolean {\n return typeof BroadcastChannel !== `undefined`\n }\n\n static isSupported(): boolean {\n return typeof BroadcastChannel !== `undefined`\n }\n\n dispose(): void {\n this.releaseLeadership()\n\n if (this.channel) {\n this.channel.removeEventListener(`message`, this.handleMessage)\n this.channel.close()\n this.channel = null\n }\n }\n}\n"],"names":["BaseLeaderElection"],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { BaseLeaderElection } from './LeaderElection.cjs';
|
|
2
|
+
export declare class BroadcastChannelLeader extends BaseLeaderElection {
|
|
3
|
+
private channelName;
|
|
4
|
+
private tabId;
|
|
5
|
+
private channel;
|
|
6
|
+
private heartbeatInterval;
|
|
7
|
+
private electionTimeout;
|
|
8
|
+
private lastLeaderHeartbeat;
|
|
9
|
+
private readonly heartbeatIntervalMs;
|
|
10
|
+
private readonly electionTimeoutMs;
|
|
11
|
+
constructor(channelName?: string);
|
|
12
|
+
private setupChannel;
|
|
13
|
+
private handleMessage;
|
|
14
|
+
requestLeadership(): Promise<boolean>;
|
|
15
|
+
private startElection;
|
|
16
|
+
private cancelElection;
|
|
17
|
+
private claimLeadership;
|
|
18
|
+
private startHeartbeat;
|
|
19
|
+
private stopHeartbeat;
|
|
20
|
+
private sendHeartbeat;
|
|
21
|
+
private sendMessage;
|
|
22
|
+
releaseLeadership(): void;
|
|
23
|
+
private isBroadcastChannelSupported;
|
|
24
|
+
static isSupported(): boolean;
|
|
25
|
+
dispose(): void;
|
|
26
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
class BaseLeaderElection {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.isLeaderState = false;
|
|
6
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
7
|
+
}
|
|
8
|
+
isLeader() {
|
|
9
|
+
return this.isLeaderState;
|
|
10
|
+
}
|
|
11
|
+
onLeadershipChange(callback) {
|
|
12
|
+
this.listeners.add(callback);
|
|
13
|
+
return () => {
|
|
14
|
+
this.listeners.delete(callback);
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
notifyLeadershipChange(isLeader) {
|
|
18
|
+
if (this.isLeaderState !== isLeader) {
|
|
19
|
+
this.isLeaderState = isLeader;
|
|
20
|
+
for (const listener of this.listeners) {
|
|
21
|
+
try {
|
|
22
|
+
listener(isLeader);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.warn(`Leadership change listener error:`, error);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
exports.BaseLeaderElection = BaseLeaderElection;
|
|
31
|
+
//# sourceMappingURL=LeaderElection.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LeaderElection.cjs","sources":["../../../src/coordination/LeaderElection.ts"],"sourcesContent":["import type { LeaderElection } from \"../types\"\n\nexport abstract class BaseLeaderElection implements LeaderElection {\n protected isLeaderState = false\n protected listeners: Set<(isLeader: boolean) => void> = new Set()\n\n abstract requestLeadership(): Promise<boolean>\n abstract releaseLeadership(): void\n\n isLeader(): boolean {\n return this.isLeaderState\n }\n\n onLeadershipChange(callback: (isLeader: boolean) => void): () => void {\n this.listeners.add(callback)\n\n return () => {\n this.listeners.delete(callback)\n }\n }\n\n protected notifyLeadershipChange(isLeader: boolean): void {\n if (this.isLeaderState !== isLeader) {\n this.isLeaderState = isLeader\n\n for (const listener of this.listeners) {\n try {\n listener(isLeader)\n } catch (error) {\n console.warn(`Leadership change listener error:`, error)\n }\n }\n }\n }\n}\n"],"names":[],"mappings":";;AAEO,MAAe,mBAA6C;AAAA,EAA5D,cAAA;AACL,SAAU,gBAAgB;AAC1B,SAAU,gCAAkD,IAAA;AAAA,EAAI;AAAA,EAKhE,WAAoB;AAClB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,mBAAmB,UAAmD;AACpE,SAAK,UAAU,IAAI,QAAQ;AAE3B,WAAO,MAAM;AACX,WAAK,UAAU,OAAO,QAAQ;AAAA,IAChC;AAAA,EACF;AAAA,EAEU,uBAAuB,UAAyB;AACxD,QAAI,KAAK,kBAAkB,UAAU;AACnC,WAAK,gBAAgB;AAErB,iBAAW,YAAY,KAAK,WAAW;AACrC,YAAI;AACF,mBAAS,QAAQ;AAAA,QACnB,SAAS,OAAO;AACd,kBAAQ,KAAK,qCAAqC,KAAK;AAAA,QACzD;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;;"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { LeaderElection } from '../types.cjs';
|
|
2
|
+
export declare abstract class BaseLeaderElection implements LeaderElection {
|
|
3
|
+
protected isLeaderState: boolean;
|
|
4
|
+
protected listeners: Set<(isLeader: boolean) => void>;
|
|
5
|
+
abstract requestLeadership(): Promise<boolean>;
|
|
6
|
+
abstract releaseLeadership(): void;
|
|
7
|
+
isLeader(): boolean;
|
|
8
|
+
onLeadershipChange(callback: (isLeader: boolean) => void): () => void;
|
|
9
|
+
protected notifyLeadershipChange(isLeader: boolean): void;
|
|
10
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const LeaderElection = require("./LeaderElection.cjs");
|
|
4
|
+
class WebLocksLeader extends LeaderElection.BaseLeaderElection {
|
|
5
|
+
constructor(lockName = `offline-executor-leader`) {
|
|
6
|
+
super();
|
|
7
|
+
this.releaseLock = null;
|
|
8
|
+
this.lockName = lockName;
|
|
9
|
+
}
|
|
10
|
+
async requestLeadership() {
|
|
11
|
+
if (!this.isWebLocksSupported()) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
if (this.isLeaderState) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const available = await navigator.locks.request(
|
|
19
|
+
this.lockName,
|
|
20
|
+
{
|
|
21
|
+
mode: `exclusive`,
|
|
22
|
+
ifAvailable: true
|
|
23
|
+
},
|
|
24
|
+
(lock) => {
|
|
25
|
+
return lock !== null;
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
if (!available) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
navigator.locks.request(
|
|
32
|
+
this.lockName,
|
|
33
|
+
{
|
|
34
|
+
mode: `exclusive`
|
|
35
|
+
},
|
|
36
|
+
async (lock) => {
|
|
37
|
+
if (lock) {
|
|
38
|
+
this.notifyLeadershipChange(true);
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
this.releaseLock = () => {
|
|
41
|
+
this.notifyLeadershipChange(false);
|
|
42
|
+
resolve();
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
return true;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (error instanceof Error && error.name === `AbortError`) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
console.warn(`Web Locks leadership request failed:`, error);
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
releaseLeadership() {
|
|
58
|
+
if (this.releaseLock) {
|
|
59
|
+
this.releaseLock();
|
|
60
|
+
this.releaseLock = null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
isWebLocksSupported() {
|
|
64
|
+
return typeof navigator !== `undefined` && `locks` in navigator;
|
|
65
|
+
}
|
|
66
|
+
static isSupported() {
|
|
67
|
+
return typeof navigator !== `undefined` && `locks` in navigator;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
exports.WebLocksLeader = WebLocksLeader;
|
|
71
|
+
//# sourceMappingURL=WebLocksLeader.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WebLocksLeader.cjs","sources":["../../../src/coordination/WebLocksLeader.ts"],"sourcesContent":["import { BaseLeaderElection } from \"./LeaderElection\"\n\nexport class WebLocksLeader extends BaseLeaderElection {\n private lockName: string\n private releaseLock: (() => void) | null = null\n\n constructor(lockName = `offline-executor-leader`) {\n super()\n this.lockName = lockName\n }\n\n async requestLeadership(): Promise<boolean> {\n if (!this.isWebLocksSupported()) {\n return false\n }\n\n if (this.isLeaderState) {\n return true\n }\n\n try {\n // First try to acquire the lock with ifAvailable\n const available = await navigator.locks.request(\n this.lockName,\n {\n mode: `exclusive`,\n ifAvailable: true,\n },\n (lock) => {\n return lock !== null\n }\n )\n\n if (!available) {\n return false\n }\n\n // Lock is available, now acquire it for real and hold it\n navigator.locks.request(\n this.lockName,\n {\n mode: `exclusive`,\n },\n async (lock) => {\n if (lock) {\n this.notifyLeadershipChange(true)\n // Hold the lock until released\n return new Promise<void>((resolve) => {\n this.releaseLock = () => {\n this.notifyLeadershipChange(false)\n resolve()\n }\n })\n }\n }\n )\n\n return true\n } catch (error) {\n if (error instanceof Error && error.name === `AbortError`) {\n return false\n }\n console.warn(`Web Locks leadership request failed:`, error)\n return false\n }\n }\n\n releaseLeadership(): void {\n if (this.releaseLock) {\n this.releaseLock()\n this.releaseLock = null\n }\n }\n\n private isWebLocksSupported(): boolean {\n return typeof navigator !== `undefined` && `locks` in navigator\n }\n\n static isSupported(): boolean {\n return typeof navigator !== `undefined` && `locks` in navigator\n }\n}\n"],"names":["BaseLeaderElection"],"mappings":";;;AAEO,MAAM,uBAAuBA,eAAAA,mBAAmB;AAAA,EAIrD,YAAY,WAAW,2BAA2B;AAChD,UAAA;AAHF,SAAQ,cAAmC;AAIzC,SAAK,WAAW;AAAA,EAClB;AAAA,EAEA,MAAM,oBAAsC;AAC1C,QAAI,CAAC,KAAK,uBAAuB;AAC/B,aAAO;AAAA,IACT;AAEA,QAAI,KAAK,eAAe;AACtB,aAAO;AAAA,IACT;AAEA,QAAI;AAEF,YAAM,YAAY,MAAM,UAAU,MAAM;AAAA,QACtC,KAAK;AAAA,QACL;AAAA,UACE,MAAM;AAAA,UACN,aAAa;AAAA,QAAA;AAAA,QAEf,CAAC,SAAS;AACR,iBAAO,SAAS;AAAA,QAClB;AAAA,MAAA;AAGF,UAAI,CAAC,WAAW;AACd,eAAO;AAAA,MACT;AAGA,gBAAU,MAAM;AAAA,QACd,KAAK;AAAA,QACL;AAAA,UACE,MAAM;AAAA,QAAA;AAAA,QAER,OAAO,SAAS;AACd,cAAI,MAAM;AACR,iBAAK,uBAAuB,IAAI;AAEhC,mBAAO,IAAI,QAAc,CAAC,YAAY;AACpC,mBAAK,cAAc,MAAM;AACvB,qBAAK,uBAAuB,KAAK;AACjC,wBAAA;AAAA,cACF;AAAA,YACF,CAAC;AAAA,UACH;AAAA,QACF;AAAA,MAAA;AAGF,aAAO;AAAA,IACT,SAAS,OAAO;AACd,UAAI,iBAAiB,SAAS,MAAM,SAAS,cAAc;AACzD,eAAO;AAAA,MACT;AACA,cAAQ,KAAK,wCAAwC,KAAK;AAC1D,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,oBAA0B;AACxB,QAAI,KAAK,aAAa;AACpB,WAAK,YAAA;AACL,WAAK,cAAc;AAAA,IACrB;AAAA,EACF;AAAA,EAEQ,sBAA+B;AACrC,WAAO,OAAO,cAAc,eAAe,WAAW;AAAA,EACxD;AAAA,EAEA,OAAO,cAAuB;AAC5B,WAAO,OAAO,cAAc,eAAe,WAAW;AAAA,EACxD;AACF;;"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { BaseLeaderElection } from './LeaderElection.cjs';
|
|
2
|
+
export declare class WebLocksLeader extends BaseLeaderElection {
|
|
3
|
+
private lockName;
|
|
4
|
+
private releaseLock;
|
|
5
|
+
constructor(lockName?: string);
|
|
6
|
+
requestLeadership(): Promise<boolean>;
|
|
7
|
+
releaseLeadership(): void;
|
|
8
|
+
private isWebLocksSupported;
|
|
9
|
+
static isSupported(): boolean;
|
|
10
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const tracer = require("../telemetry/tracer.cjs");
|
|
4
|
+
class KeyScheduler {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.pendingTransactions = [];
|
|
7
|
+
this.isRunning = false;
|
|
8
|
+
}
|
|
9
|
+
schedule(transaction) {
|
|
10
|
+
tracer.withSyncSpan(
|
|
11
|
+
`scheduler.schedule`,
|
|
12
|
+
{
|
|
13
|
+
"transaction.id": transaction.id,
|
|
14
|
+
queueLength: this.pendingTransactions.length
|
|
15
|
+
},
|
|
16
|
+
() => {
|
|
17
|
+
this.pendingTransactions.push(transaction);
|
|
18
|
+
this.pendingTransactions.sort(
|
|
19
|
+
(a, b) => a.createdAt.getTime() - b.createdAt.getTime()
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
getNextBatch(_maxConcurrency) {
|
|
25
|
+
return tracer.withSyncSpan(
|
|
26
|
+
`scheduler.getNextBatch`,
|
|
27
|
+
{ pendingCount: this.pendingTransactions.length },
|
|
28
|
+
(span) => {
|
|
29
|
+
if (this.isRunning || this.pendingTransactions.length === 0) {
|
|
30
|
+
span.setAttribute(`result`, `empty`);
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
const readyTransaction = this.pendingTransactions.find(
|
|
34
|
+
(tx) => this.isReadyToRun(tx)
|
|
35
|
+
);
|
|
36
|
+
if (readyTransaction) {
|
|
37
|
+
span.setAttribute(`result`, `found`);
|
|
38
|
+
span.setAttribute(`transaction.id`, readyTransaction.id);
|
|
39
|
+
} else {
|
|
40
|
+
span.setAttribute(`result`, `none_ready`);
|
|
41
|
+
}
|
|
42
|
+
return readyTransaction ? [readyTransaction] : [];
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
isReadyToRun(transaction) {
|
|
47
|
+
return Date.now() >= transaction.nextAttemptAt;
|
|
48
|
+
}
|
|
49
|
+
markStarted(_transaction) {
|
|
50
|
+
this.isRunning = true;
|
|
51
|
+
}
|
|
52
|
+
markCompleted(transaction) {
|
|
53
|
+
this.removeTransaction(transaction);
|
|
54
|
+
this.isRunning = false;
|
|
55
|
+
}
|
|
56
|
+
markFailed(_transaction) {
|
|
57
|
+
this.isRunning = false;
|
|
58
|
+
}
|
|
59
|
+
removeTransaction(transaction) {
|
|
60
|
+
const index = this.pendingTransactions.findIndex(
|
|
61
|
+
(tx) => tx.id === transaction.id
|
|
62
|
+
);
|
|
63
|
+
if (index >= 0) {
|
|
64
|
+
this.pendingTransactions.splice(index, 1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
updateTransaction(transaction) {
|
|
68
|
+
const index = this.pendingTransactions.findIndex(
|
|
69
|
+
(tx) => tx.id === transaction.id
|
|
70
|
+
);
|
|
71
|
+
if (index >= 0) {
|
|
72
|
+
this.pendingTransactions[index] = transaction;
|
|
73
|
+
this.pendingTransactions.sort(
|
|
74
|
+
(a, b) => a.createdAt.getTime() - b.createdAt.getTime()
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
getPendingCount() {
|
|
79
|
+
return this.pendingTransactions.length;
|
|
80
|
+
}
|
|
81
|
+
getRunningCount() {
|
|
82
|
+
return this.isRunning ? 1 : 0;
|
|
83
|
+
}
|
|
84
|
+
clear() {
|
|
85
|
+
this.pendingTransactions = [];
|
|
86
|
+
this.isRunning = false;
|
|
87
|
+
}
|
|
88
|
+
getAllPendingTransactions() {
|
|
89
|
+
return [...this.pendingTransactions];
|
|
90
|
+
}
|
|
91
|
+
updateTransactions(updatedTransactions) {
|
|
92
|
+
for (const updatedTx of updatedTransactions) {
|
|
93
|
+
const index = this.pendingTransactions.findIndex(
|
|
94
|
+
(tx) => tx.id === updatedTx.id
|
|
95
|
+
);
|
|
96
|
+
if (index >= 0) {
|
|
97
|
+
this.pendingTransactions[index] = updatedTx;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
this.pendingTransactions.sort(
|
|
101
|
+
(a, b) => a.createdAt.getTime() - b.createdAt.getTime()
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
exports.KeyScheduler = KeyScheduler;
|
|
106
|
+
//# sourceMappingURL=KeyScheduler.cjs.map
|