@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,73 @@
|
|
|
1
|
+
class DefaultOnlineDetector {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
4
|
+
this.isListening = false;
|
|
5
|
+
this.handleOnline = () => {
|
|
6
|
+
this.notifyListeners();
|
|
7
|
+
};
|
|
8
|
+
this.handleVisibilityChange = () => {
|
|
9
|
+
if (document.visibilityState === `visible`) {
|
|
10
|
+
this.notifyListeners();
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
this.startListening();
|
|
14
|
+
}
|
|
15
|
+
startListening() {
|
|
16
|
+
if (this.isListening) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
this.isListening = true;
|
|
20
|
+
if (typeof window !== `undefined`) {
|
|
21
|
+
window.addEventListener(`online`, this.handleOnline);
|
|
22
|
+
document.addEventListener(`visibilitychange`, this.handleVisibilityChange);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
stopListening() {
|
|
26
|
+
if (!this.isListening) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
this.isListening = false;
|
|
30
|
+
if (typeof window !== `undefined`) {
|
|
31
|
+
window.removeEventListener(`online`, this.handleOnline);
|
|
32
|
+
document.removeEventListener(
|
|
33
|
+
`visibilitychange`,
|
|
34
|
+
this.handleVisibilityChange
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
notifyListeners() {
|
|
39
|
+
for (const listener of this.listeners) {
|
|
40
|
+
try {
|
|
41
|
+
listener();
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.warn(`OnlineDetector listener error:`, error);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
subscribe(callback) {
|
|
48
|
+
this.listeners.add(callback);
|
|
49
|
+
return () => {
|
|
50
|
+
this.listeners.delete(callback);
|
|
51
|
+
if (this.listeners.size === 0) {
|
|
52
|
+
this.stopListening();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
notifyOnline() {
|
|
57
|
+
this.notifyListeners();
|
|
58
|
+
}
|
|
59
|
+
isOnline() {
|
|
60
|
+
if (typeof navigator !== `undefined`) {
|
|
61
|
+
return navigator.onLine;
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
dispose() {
|
|
66
|
+
this.stopListening();
|
|
67
|
+
this.listeners.clear();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export {
|
|
71
|
+
DefaultOnlineDetector
|
|
72
|
+
};
|
|
73
|
+
//# sourceMappingURL=OnlineDetector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OnlineDetector.js","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,26 @@
|
|
|
1
|
+
import { BaseLeaderElection } from './LeaderElection.js';
|
|
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,146 @@
|
|
|
1
|
+
import { BaseLeaderElection } from "./LeaderElection.js";
|
|
2
|
+
class BroadcastChannelLeader extends BaseLeaderElection {
|
|
3
|
+
constructor(channelName = `offline-executor-leader`) {
|
|
4
|
+
super();
|
|
5
|
+
this.channel = null;
|
|
6
|
+
this.heartbeatInterval = null;
|
|
7
|
+
this.electionTimeout = null;
|
|
8
|
+
this.lastLeaderHeartbeat = 0;
|
|
9
|
+
this.heartbeatIntervalMs = 5e3;
|
|
10
|
+
this.electionTimeoutMs = 1e4;
|
|
11
|
+
this.handleMessage = (event) => {
|
|
12
|
+
const { type, tabId, timestamp } = event.data;
|
|
13
|
+
if (tabId === this.tabId) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
switch (type) {
|
|
17
|
+
case `heartbeat`:
|
|
18
|
+
if (this.isLeaderState && tabId < this.tabId) {
|
|
19
|
+
this.releaseLeadership();
|
|
20
|
+
} else if (!this.isLeaderState) {
|
|
21
|
+
this.lastLeaderHeartbeat = timestamp;
|
|
22
|
+
this.cancelElection();
|
|
23
|
+
}
|
|
24
|
+
break;
|
|
25
|
+
case `election`:
|
|
26
|
+
if (this.isLeaderState) {
|
|
27
|
+
this.sendHeartbeat();
|
|
28
|
+
} else if (tabId > this.tabId) {
|
|
29
|
+
this.startElection();
|
|
30
|
+
}
|
|
31
|
+
break;
|
|
32
|
+
case `leadership-claim`:
|
|
33
|
+
if (this.isLeaderState && tabId < this.tabId) {
|
|
34
|
+
this.releaseLeadership();
|
|
35
|
+
}
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
this.channelName = channelName;
|
|
40
|
+
this.tabId = crypto.randomUUID();
|
|
41
|
+
this.setupChannel();
|
|
42
|
+
}
|
|
43
|
+
setupChannel() {
|
|
44
|
+
if (!this.isBroadcastChannelSupported()) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
this.channel = new BroadcastChannel(this.channelName);
|
|
48
|
+
this.channel.addEventListener(`message`, this.handleMessage);
|
|
49
|
+
}
|
|
50
|
+
async requestLeadership() {
|
|
51
|
+
if (!this.isBroadcastChannelSupported()) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
if (this.isLeaderState) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
this.startElection();
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
setTimeout(() => {
|
|
60
|
+
resolve(this.isLeaderState);
|
|
61
|
+
}, 1e3);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
startElection() {
|
|
65
|
+
if (this.electionTimeout) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
this.sendMessage({
|
|
69
|
+
type: `election`,
|
|
70
|
+
tabId: this.tabId,
|
|
71
|
+
timestamp: Date.now()
|
|
72
|
+
});
|
|
73
|
+
this.electionTimeout = window.setTimeout(() => {
|
|
74
|
+
const timeSinceLastHeartbeat = Date.now() - this.lastLeaderHeartbeat;
|
|
75
|
+
if (timeSinceLastHeartbeat > this.electionTimeoutMs) {
|
|
76
|
+
this.claimLeadership();
|
|
77
|
+
}
|
|
78
|
+
this.electionTimeout = null;
|
|
79
|
+
}, this.electionTimeoutMs);
|
|
80
|
+
}
|
|
81
|
+
cancelElection() {
|
|
82
|
+
if (this.electionTimeout) {
|
|
83
|
+
clearTimeout(this.electionTimeout);
|
|
84
|
+
this.electionTimeout = null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
claimLeadership() {
|
|
88
|
+
this.notifyLeadershipChange(true);
|
|
89
|
+
this.sendMessage({
|
|
90
|
+
type: `leadership-claim`,
|
|
91
|
+
tabId: this.tabId,
|
|
92
|
+
timestamp: Date.now()
|
|
93
|
+
});
|
|
94
|
+
this.startHeartbeat();
|
|
95
|
+
}
|
|
96
|
+
startHeartbeat() {
|
|
97
|
+
if (this.heartbeatInterval) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
this.sendHeartbeat();
|
|
101
|
+
this.heartbeatInterval = window.setInterval(() => {
|
|
102
|
+
this.sendHeartbeat();
|
|
103
|
+
}, this.heartbeatIntervalMs);
|
|
104
|
+
}
|
|
105
|
+
stopHeartbeat() {
|
|
106
|
+
if (this.heartbeatInterval) {
|
|
107
|
+
clearInterval(this.heartbeatInterval);
|
|
108
|
+
this.heartbeatInterval = null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
sendHeartbeat() {
|
|
112
|
+
this.sendMessage({
|
|
113
|
+
type: `heartbeat`,
|
|
114
|
+
tabId: this.tabId,
|
|
115
|
+
timestamp: Date.now()
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
sendMessage(message) {
|
|
119
|
+
if (this.channel) {
|
|
120
|
+
this.channel.postMessage(message);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
releaseLeadership() {
|
|
124
|
+
this.stopHeartbeat();
|
|
125
|
+
this.cancelElection();
|
|
126
|
+
this.notifyLeadershipChange(false);
|
|
127
|
+
}
|
|
128
|
+
isBroadcastChannelSupported() {
|
|
129
|
+
return typeof BroadcastChannel !== `undefined`;
|
|
130
|
+
}
|
|
131
|
+
static isSupported() {
|
|
132
|
+
return typeof BroadcastChannel !== `undefined`;
|
|
133
|
+
}
|
|
134
|
+
dispose() {
|
|
135
|
+
this.releaseLeadership();
|
|
136
|
+
if (this.channel) {
|
|
137
|
+
this.channel.removeEventListener(`message`, this.handleMessage);
|
|
138
|
+
this.channel.close();
|
|
139
|
+
this.channel = null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
export {
|
|
144
|
+
BroadcastChannelLeader
|
|
145
|
+
};
|
|
146
|
+
//# sourceMappingURL=BroadcastChannelLeader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BroadcastChannelLeader.js","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":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { LeaderElection } from '../types.js';
|
|
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,31 @@
|
|
|
1
|
+
class BaseLeaderElection {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.isLeaderState = false;
|
|
4
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
5
|
+
}
|
|
6
|
+
isLeader() {
|
|
7
|
+
return this.isLeaderState;
|
|
8
|
+
}
|
|
9
|
+
onLeadershipChange(callback) {
|
|
10
|
+
this.listeners.add(callback);
|
|
11
|
+
return () => {
|
|
12
|
+
this.listeners.delete(callback);
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
notifyLeadershipChange(isLeader) {
|
|
16
|
+
if (this.isLeaderState !== isLeader) {
|
|
17
|
+
this.isLeaderState = isLeader;
|
|
18
|
+
for (const listener of this.listeners) {
|
|
19
|
+
try {
|
|
20
|
+
listener(isLeader);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.warn(`Leadership change listener error:`, error);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export {
|
|
29
|
+
BaseLeaderElection
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=LeaderElection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LeaderElection.js","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 { BaseLeaderElection } from './LeaderElection.js';
|
|
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,71 @@
|
|
|
1
|
+
import { BaseLeaderElection } from "./LeaderElection.js";
|
|
2
|
+
class WebLocksLeader extends BaseLeaderElection {
|
|
3
|
+
constructor(lockName = `offline-executor-leader`) {
|
|
4
|
+
super();
|
|
5
|
+
this.releaseLock = null;
|
|
6
|
+
this.lockName = lockName;
|
|
7
|
+
}
|
|
8
|
+
async requestLeadership() {
|
|
9
|
+
if (!this.isWebLocksSupported()) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
if (this.isLeaderState) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const available = await navigator.locks.request(
|
|
17
|
+
this.lockName,
|
|
18
|
+
{
|
|
19
|
+
mode: `exclusive`,
|
|
20
|
+
ifAvailable: true
|
|
21
|
+
},
|
|
22
|
+
(lock) => {
|
|
23
|
+
return lock !== null;
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
if (!available) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
navigator.locks.request(
|
|
30
|
+
this.lockName,
|
|
31
|
+
{
|
|
32
|
+
mode: `exclusive`
|
|
33
|
+
},
|
|
34
|
+
async (lock) => {
|
|
35
|
+
if (lock) {
|
|
36
|
+
this.notifyLeadershipChange(true);
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
this.releaseLock = () => {
|
|
39
|
+
this.notifyLeadershipChange(false);
|
|
40
|
+
resolve();
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
return true;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (error instanceof Error && error.name === `AbortError`) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
console.warn(`Web Locks leadership request failed:`, error);
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
releaseLeadership() {
|
|
56
|
+
if (this.releaseLock) {
|
|
57
|
+
this.releaseLock();
|
|
58
|
+
this.releaseLock = null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
isWebLocksSupported() {
|
|
62
|
+
return typeof navigator !== `undefined` && `locks` in navigator;
|
|
63
|
+
}
|
|
64
|
+
static isSupported() {
|
|
65
|
+
return typeof navigator !== `undefined` && `locks` in navigator;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export {
|
|
69
|
+
WebLocksLeader
|
|
70
|
+
};
|
|
71
|
+
//# sourceMappingURL=WebLocksLeader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WebLocksLeader.js","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":[],"mappings":";AAEO,MAAM,uBAAuB,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,18 @@
|
|
|
1
|
+
import { OfflineTransaction } from '../types.js';
|
|
2
|
+
export declare class KeyScheduler {
|
|
3
|
+
private pendingTransactions;
|
|
4
|
+
private isRunning;
|
|
5
|
+
schedule(transaction: OfflineTransaction): void;
|
|
6
|
+
getNextBatch(_maxConcurrency: number): Array<OfflineTransaction>;
|
|
7
|
+
private isReadyToRun;
|
|
8
|
+
markStarted(_transaction: OfflineTransaction): void;
|
|
9
|
+
markCompleted(transaction: OfflineTransaction): void;
|
|
10
|
+
markFailed(_transaction: OfflineTransaction): void;
|
|
11
|
+
private removeTransaction;
|
|
12
|
+
updateTransaction(transaction: OfflineTransaction): void;
|
|
13
|
+
getPendingCount(): number;
|
|
14
|
+
getRunningCount(): number;
|
|
15
|
+
clear(): void;
|
|
16
|
+
getAllPendingTransactions(): Array<OfflineTransaction>;
|
|
17
|
+
updateTransactions(updatedTransactions: Array<OfflineTransaction>): void;
|
|
18
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { withSyncSpan } from "../telemetry/tracer.js";
|
|
2
|
+
class KeyScheduler {
|
|
3
|
+
constructor() {
|
|
4
|
+
this.pendingTransactions = [];
|
|
5
|
+
this.isRunning = false;
|
|
6
|
+
}
|
|
7
|
+
schedule(transaction) {
|
|
8
|
+
withSyncSpan(
|
|
9
|
+
`scheduler.schedule`,
|
|
10
|
+
{
|
|
11
|
+
"transaction.id": transaction.id,
|
|
12
|
+
queueLength: this.pendingTransactions.length
|
|
13
|
+
},
|
|
14
|
+
() => {
|
|
15
|
+
this.pendingTransactions.push(transaction);
|
|
16
|
+
this.pendingTransactions.sort(
|
|
17
|
+
(a, b) => a.createdAt.getTime() - b.createdAt.getTime()
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
getNextBatch(_maxConcurrency) {
|
|
23
|
+
return withSyncSpan(
|
|
24
|
+
`scheduler.getNextBatch`,
|
|
25
|
+
{ pendingCount: this.pendingTransactions.length },
|
|
26
|
+
(span) => {
|
|
27
|
+
if (this.isRunning || this.pendingTransactions.length === 0) {
|
|
28
|
+
span.setAttribute(`result`, `empty`);
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
const readyTransaction = this.pendingTransactions.find(
|
|
32
|
+
(tx) => this.isReadyToRun(tx)
|
|
33
|
+
);
|
|
34
|
+
if (readyTransaction) {
|
|
35
|
+
span.setAttribute(`result`, `found`);
|
|
36
|
+
span.setAttribute(`transaction.id`, readyTransaction.id);
|
|
37
|
+
} else {
|
|
38
|
+
span.setAttribute(`result`, `none_ready`);
|
|
39
|
+
}
|
|
40
|
+
return readyTransaction ? [readyTransaction] : [];
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
isReadyToRun(transaction) {
|
|
45
|
+
return Date.now() >= transaction.nextAttemptAt;
|
|
46
|
+
}
|
|
47
|
+
markStarted(_transaction) {
|
|
48
|
+
this.isRunning = true;
|
|
49
|
+
}
|
|
50
|
+
markCompleted(transaction) {
|
|
51
|
+
this.removeTransaction(transaction);
|
|
52
|
+
this.isRunning = false;
|
|
53
|
+
}
|
|
54
|
+
markFailed(_transaction) {
|
|
55
|
+
this.isRunning = false;
|
|
56
|
+
}
|
|
57
|
+
removeTransaction(transaction) {
|
|
58
|
+
const index = this.pendingTransactions.findIndex(
|
|
59
|
+
(tx) => tx.id === transaction.id
|
|
60
|
+
);
|
|
61
|
+
if (index >= 0) {
|
|
62
|
+
this.pendingTransactions.splice(index, 1);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
updateTransaction(transaction) {
|
|
66
|
+
const index = this.pendingTransactions.findIndex(
|
|
67
|
+
(tx) => tx.id === transaction.id
|
|
68
|
+
);
|
|
69
|
+
if (index >= 0) {
|
|
70
|
+
this.pendingTransactions[index] = transaction;
|
|
71
|
+
this.pendingTransactions.sort(
|
|
72
|
+
(a, b) => a.createdAt.getTime() - b.createdAt.getTime()
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
getPendingCount() {
|
|
77
|
+
return this.pendingTransactions.length;
|
|
78
|
+
}
|
|
79
|
+
getRunningCount() {
|
|
80
|
+
return this.isRunning ? 1 : 0;
|
|
81
|
+
}
|
|
82
|
+
clear() {
|
|
83
|
+
this.pendingTransactions = [];
|
|
84
|
+
this.isRunning = false;
|
|
85
|
+
}
|
|
86
|
+
getAllPendingTransactions() {
|
|
87
|
+
return [...this.pendingTransactions];
|
|
88
|
+
}
|
|
89
|
+
updateTransactions(updatedTransactions) {
|
|
90
|
+
for (const updatedTx of updatedTransactions) {
|
|
91
|
+
const index = this.pendingTransactions.findIndex(
|
|
92
|
+
(tx) => tx.id === updatedTx.id
|
|
93
|
+
);
|
|
94
|
+
if (index >= 0) {
|
|
95
|
+
this.pendingTransactions[index] = updatedTx;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
this.pendingTransactions.sort(
|
|
99
|
+
(a, b) => a.createdAt.getTime() - b.createdAt.getTime()
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export {
|
|
104
|
+
KeyScheduler
|
|
105
|
+
};
|
|
106
|
+
//# sourceMappingURL=KeyScheduler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"KeyScheduler.js","sources":["../../../src/executor/KeyScheduler.ts"],"sourcesContent":["import { withSyncSpan } from \"../telemetry/tracer\"\nimport type { OfflineTransaction } from \"../types\"\n\nexport class KeyScheduler {\n private pendingTransactions: Array<OfflineTransaction> = []\n private isRunning = false\n\n schedule(transaction: OfflineTransaction): void {\n withSyncSpan(\n `scheduler.schedule`,\n {\n \"transaction.id\": transaction.id,\n queueLength: this.pendingTransactions.length,\n },\n () => {\n this.pendingTransactions.push(transaction)\n // Sort by creation time to maintain FIFO order\n this.pendingTransactions.sort(\n (a, b) => a.createdAt.getTime() - b.createdAt.getTime()\n )\n }\n )\n }\n\n getNextBatch(_maxConcurrency: number): Array<OfflineTransaction> {\n return withSyncSpan(\n `scheduler.getNextBatch`,\n { pendingCount: this.pendingTransactions.length },\n (span) => {\n // For sequential processing, we ignore maxConcurrency and only process one transaction at a time\n if (this.isRunning || this.pendingTransactions.length === 0) {\n span.setAttribute(`result`, `empty`)\n return []\n }\n\n // Find the first transaction that's ready to run\n const readyTransaction = this.pendingTransactions.find((tx) =>\n this.isReadyToRun(tx)\n )\n\n if (readyTransaction) {\n span.setAttribute(`result`, `found`)\n span.setAttribute(`transaction.id`, readyTransaction.id)\n } else {\n span.setAttribute(`result`, `none_ready`)\n }\n\n return readyTransaction ? [readyTransaction] : []\n }\n )\n }\n\n private isReadyToRun(transaction: OfflineTransaction): boolean {\n return Date.now() >= transaction.nextAttemptAt\n }\n\n markStarted(_transaction: OfflineTransaction): void {\n this.isRunning = true\n }\n\n markCompleted(transaction: OfflineTransaction): void {\n this.removeTransaction(transaction)\n this.isRunning = false\n }\n\n markFailed(_transaction: OfflineTransaction): void {\n this.isRunning = false\n }\n\n private removeTransaction(transaction: OfflineTransaction): void {\n const index = this.pendingTransactions.findIndex(\n (tx) => tx.id === transaction.id\n )\n if (index >= 0) {\n this.pendingTransactions.splice(index, 1)\n }\n }\n\n updateTransaction(transaction: OfflineTransaction): void {\n const index = this.pendingTransactions.findIndex(\n (tx) => tx.id === transaction.id\n )\n if (index >= 0) {\n this.pendingTransactions[index] = transaction\n // Re-sort to maintain FIFO order after update\n this.pendingTransactions.sort(\n (a, b) => a.createdAt.getTime() - b.createdAt.getTime()\n )\n }\n }\n\n getPendingCount(): number {\n return this.pendingTransactions.length\n }\n\n getRunningCount(): number {\n return this.isRunning ? 1 : 0\n }\n\n clear(): void {\n this.pendingTransactions = []\n this.isRunning = false\n }\n\n getAllPendingTransactions(): Array<OfflineTransaction> {\n return [...this.pendingTransactions]\n }\n\n updateTransactions(updatedTransactions: Array<OfflineTransaction>): void {\n for (const updatedTx of updatedTransactions) {\n const index = this.pendingTransactions.findIndex(\n (tx) => tx.id === updatedTx.id\n )\n if (index >= 0) {\n this.pendingTransactions[index] = updatedTx\n }\n }\n // Re-sort to maintain FIFO order after updates\n this.pendingTransactions.sort(\n (a, b) => a.createdAt.getTime() - b.createdAt.getTime()\n )\n }\n}\n"],"names":[],"mappings":";AAGO,MAAM,aAAa;AAAA,EAAnB,cAAA;AACL,SAAQ,sBAAiD,CAAA;AACzD,SAAQ,YAAY;AAAA,EAAA;AAAA,EAEpB,SAAS,aAAuC;AAC9C;AAAA,MACE;AAAA,MACA;AAAA,QACE,kBAAkB,YAAY;AAAA,QAC9B,aAAa,KAAK,oBAAoB;AAAA,MAAA;AAAA,MAExC,MAAM;AACJ,aAAK,oBAAoB,KAAK,WAAW;AAEzC,aAAK,oBAAoB;AAAA,UACvB,CAAC,GAAG,MAAM,EAAE,UAAU,YAAY,EAAE,UAAU,QAAA;AAAA,QAAQ;AAAA,MAE1D;AAAA,IAAA;AAAA,EAEJ;AAAA,EAEA,aAAa,iBAAoD;AAC/D,WAAO;AAAA,MACL;AAAA,MACA,EAAE,cAAc,KAAK,oBAAoB,OAAA;AAAA,MACzC,CAAC,SAAS;AAER,YAAI,KAAK,aAAa,KAAK,oBAAoB,WAAW,GAAG;AAC3D,eAAK,aAAa,UAAU,OAAO;AACnC,iBAAO,CAAA;AAAA,QACT;AAGA,cAAM,mBAAmB,KAAK,oBAAoB;AAAA,UAAK,CAAC,OACtD,KAAK,aAAa,EAAE;AAAA,QAAA;AAGtB,YAAI,kBAAkB;AACpB,eAAK,aAAa,UAAU,OAAO;AACnC,eAAK,aAAa,kBAAkB,iBAAiB,EAAE;AAAA,QACzD,OAAO;AACL,eAAK,aAAa,UAAU,YAAY;AAAA,QAC1C;AAEA,eAAO,mBAAmB,CAAC,gBAAgB,IAAI,CAAA;AAAA,MACjD;AAAA,IAAA;AAAA,EAEJ;AAAA,EAEQ,aAAa,aAA0C;AAC7D,WAAO,KAAK,SAAS,YAAY;AAAA,EACnC;AAAA,EAEA,YAAY,cAAwC;AAClD,SAAK,YAAY;AAAA,EACnB;AAAA,EAEA,cAAc,aAAuC;AACnD,SAAK,kBAAkB,WAAW;AAClC,SAAK,YAAY;AAAA,EACnB;AAAA,EAEA,WAAW,cAAwC;AACjD,SAAK,YAAY;AAAA,EACnB;AAAA,EAEQ,kBAAkB,aAAuC;AAC/D,UAAM,QAAQ,KAAK,oBAAoB;AAAA,MACrC,CAAC,OAAO,GAAG,OAAO,YAAY;AAAA,IAAA;AAEhC,QAAI,SAAS,GAAG;AACd,WAAK,oBAAoB,OAAO,OAAO,CAAC;AAAA,IAC1C;AAAA,EACF;AAAA,EAEA,kBAAkB,aAAuC;AACvD,UAAM,QAAQ,KAAK,oBAAoB;AAAA,MACrC,CAAC,OAAO,GAAG,OAAO,YAAY;AAAA,IAAA;AAEhC,QAAI,SAAS,GAAG;AACd,WAAK,oBAAoB,KAAK,IAAI;AAElC,WAAK,oBAAoB;AAAA,QACvB,CAAC,GAAG,MAAM,EAAE,UAAU,YAAY,EAAE,UAAU,QAAA;AAAA,MAAQ;AAAA,IAE1D;AAAA,EACF;AAAA,EAEA,kBAA0B;AACxB,WAAO,KAAK,oBAAoB;AAAA,EAClC;AAAA,EAEA,kBAA0B;AACxB,WAAO,KAAK,YAAY,IAAI;AAAA,EAC9B;AAAA,EAEA,QAAc;AACZ,SAAK,sBAAsB,CAAA;AAC3B,SAAK,YAAY;AAAA,EACnB;AAAA,EAEA,4BAAuD;AACrD,WAAO,CAAC,GAAG,KAAK,mBAAmB;AAAA,EACrC;AAAA,EAEA,mBAAmB,qBAAsD;AACvE,eAAW,aAAa,qBAAqB;AAC3C,YAAM,QAAQ,KAAK,oBAAoB;AAAA,QACrC,CAAC,OAAO,GAAG,OAAO,UAAU;AAAA,MAAA;AAE9B,UAAI,SAAS,GAAG;AACd,aAAK,oBAAoB,KAAK,IAAI;AAAA,MACpC;AAAA,IACF;AAEA,SAAK,oBAAoB;AAAA,MACvB,CAAC,GAAG,MAAM,EAAE,UAAU,YAAY,EAAE,UAAU,QAAA;AAAA,IAAQ;AAAA,EAE1D;AACF;"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { KeyScheduler } from './KeyScheduler.js';
|
|
2
|
+
import { OutboxManager } from '../outbox/OutboxManager.js';
|
|
3
|
+
import { OfflineConfig, OfflineTransaction } from '../types.js';
|
|
4
|
+
export declare class TransactionExecutor {
|
|
5
|
+
private scheduler;
|
|
6
|
+
private outbox;
|
|
7
|
+
private config;
|
|
8
|
+
private retryPolicy;
|
|
9
|
+
private isExecuting;
|
|
10
|
+
private executionPromise;
|
|
11
|
+
private offlineExecutor;
|
|
12
|
+
private retryTimer;
|
|
13
|
+
constructor(scheduler: KeyScheduler, outbox: OutboxManager, config: OfflineConfig, offlineExecutor: any);
|
|
14
|
+
execute(transaction: OfflineTransaction): Promise<void>;
|
|
15
|
+
executeAll(): Promise<void>;
|
|
16
|
+
private runExecution;
|
|
17
|
+
private executeTransaction;
|
|
18
|
+
private runMutationFn;
|
|
19
|
+
private handleError;
|
|
20
|
+
loadPendingTransactions(): Promise<void>;
|
|
21
|
+
clear(): void;
|
|
22
|
+
getPendingCount(): number;
|
|
23
|
+
private scheduleNextRetry;
|
|
24
|
+
private getEarliestRetryTime;
|
|
25
|
+
private clearRetryTimer;
|
|
26
|
+
getRunningCount(): number;
|
|
27
|
+
resetRetryDelays(): void;
|
|
28
|
+
}
|