pending-task-kit 0.1.0 → 0.2.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/LICENSE +21 -0
- package/README.md +115 -5
- package/README.zh-CN.md +105 -6
- package/dist/{engine-ClhasLko.d.cts → engine-BJ7pokhW.d.ts} +82 -6
- package/dist/index.d.ts +2 -2
- package/dist/index.js +127 -39
- package/dist/index.js.map +1 -1
- package/dist/react.d.ts +2 -2
- package/dist/react.js +78 -13
- package/dist/react.js.map +1 -1
- package/package.json +40 -15
- package/dist/engine-ClhasLko.d.ts +0 -362
- package/dist/index.cjs +0 -705
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -122
- package/dist/react.cjs +0 -572
- package/dist/react.cjs.map +0 -1
- package/dist/react.d.cts +0 -24
package/dist/react.cjs
DELETED
|
@@ -1,572 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
-
var __export = (target, all) => {
|
|
7
|
-
for (var name in all)
|
|
8
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
-
};
|
|
10
|
-
var __copyProps = (to, from, except, desc) => {
|
|
11
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
-
for (let key of __getOwnPropNames(from))
|
|
13
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
-
|
|
20
|
-
// src/react.ts
|
|
21
|
-
var react_exports = {};
|
|
22
|
-
__export(react_exports, {
|
|
23
|
-
PendingTaskPoller: () => PendingTaskPoller,
|
|
24
|
-
usePendingTaskPoller: () => usePendingTaskPoller
|
|
25
|
-
});
|
|
26
|
-
module.exports = __toCommonJS(react_exports);
|
|
27
|
-
var import_react = require("react");
|
|
28
|
-
|
|
29
|
-
// src/safe-storage.ts
|
|
30
|
-
function safeGetItem(key) {
|
|
31
|
-
try {
|
|
32
|
-
if (typeof localStorage === "undefined") return null;
|
|
33
|
-
return localStorage.getItem(key);
|
|
34
|
-
} catch {
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
function safeSetItem(key, value) {
|
|
39
|
-
try {
|
|
40
|
-
if (typeof localStorage === "undefined") return false;
|
|
41
|
-
localStorage.setItem(key, value);
|
|
42
|
-
return true;
|
|
43
|
-
} catch {
|
|
44
|
-
return false;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// src/poll-lease.ts
|
|
49
|
-
function readLease(storageKey) {
|
|
50
|
-
const raw = safeGetItem(storageKey);
|
|
51
|
-
if (!raw) return null;
|
|
52
|
-
try {
|
|
53
|
-
const parsed = JSON.parse(raw);
|
|
54
|
-
if (typeof parsed.ownerId !== "string" || typeof parsed.expiresAt !== "number" || typeof parsed.fence !== "number") {
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
return parsed;
|
|
58
|
-
} catch {
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
function writeLease(storageKey, lease) {
|
|
63
|
-
safeSetItem(storageKey, JSON.stringify(lease));
|
|
64
|
-
}
|
|
65
|
-
function createPollLeaseClaimer(storageKey, ttlMs) {
|
|
66
|
-
if (ttlMs <= 0 && typeof console !== "undefined") {
|
|
67
|
-
console.warn(`pending-task-kit: pollLeaseTtlMs must be positive, got ${ttlMs}`);
|
|
68
|
-
}
|
|
69
|
-
return {
|
|
70
|
-
claim(ownerId) {
|
|
71
|
-
const current = readLease(storageKey);
|
|
72
|
-
const now = Date.now();
|
|
73
|
-
if (current && current.ownerId !== ownerId && current.expiresAt > now) {
|
|
74
|
-
return { leader: false };
|
|
75
|
-
}
|
|
76
|
-
const isRenewal = current !== null && current.ownerId === ownerId && current.expiresAt > now;
|
|
77
|
-
const fence = isRenewal ? current.fence : (current?.fence ?? 0) + 1;
|
|
78
|
-
writeLease(storageKey, { ownerId, fence, expiresAt: now + ttlMs });
|
|
79
|
-
return { leader: true, fence };
|
|
80
|
-
},
|
|
81
|
-
release(ownerId) {
|
|
82
|
-
const current = readLease(storageKey);
|
|
83
|
-
if (current?.ownerId === ownerId) {
|
|
84
|
-
writeLease(storageKey, { ownerId, fence: current.fence, expiresAt: 0 });
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
function generatePollOwnerId() {
|
|
90
|
-
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
91
|
-
return crypto.randomUUID();
|
|
92
|
-
}
|
|
93
|
-
return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// src/store.ts
|
|
97
|
-
var import_zustand = require("zustand");
|
|
98
|
-
var import_middleware = require("zustand/middleware");
|
|
99
|
-
var DEFAULT_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
100
|
-
var DEFAULT_STORAGE_KEY = "pending-tasks";
|
|
101
|
-
function isPendingTaskShape(value) {
|
|
102
|
-
if (!value || typeof value !== "object") return false;
|
|
103
|
-
const t = value;
|
|
104
|
-
return typeof t.id === "string" && typeof t.type === "string" && typeof t.startedAt === "number" && (typeof t.taskId === "number" || typeof t.taskId === "string");
|
|
105
|
-
}
|
|
106
|
-
function parseTasksFromStorageValue(value) {
|
|
107
|
-
if (!value) return [];
|
|
108
|
-
try {
|
|
109
|
-
const parsed = JSON.parse(value);
|
|
110
|
-
const tasks = parsed?.state?.tasks;
|
|
111
|
-
if (!Array.isArray(tasks)) return [];
|
|
112
|
-
return tasks.filter(isPendingTaskShape);
|
|
113
|
-
} catch {
|
|
114
|
-
return [];
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
function readPersistedTasks(storageKey) {
|
|
118
|
-
if (typeof localStorage === "undefined") return [];
|
|
119
|
-
return parseTasksFromStorageValue(localStorage.getItem(storageKey));
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// src/result-relay.ts
|
|
123
|
-
var RESULT_STATUSES = [
|
|
124
|
-
"success",
|
|
125
|
-
"failure",
|
|
126
|
-
"error",
|
|
127
|
-
"expired"
|
|
128
|
-
];
|
|
129
|
-
function isResultStatus(value) {
|
|
130
|
-
return typeof value === "string" && RESULT_STATUSES.includes(value);
|
|
131
|
-
}
|
|
132
|
-
function writeResultRelay(storageKey, detail) {
|
|
133
|
-
try {
|
|
134
|
-
const serialized = JSON.stringify(detail);
|
|
135
|
-
safeSetItem(storageKey, serialized);
|
|
136
|
-
} catch {
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
function parseResultRelay(value) {
|
|
140
|
-
if (!value) return null;
|
|
141
|
-
try {
|
|
142
|
-
const parsed = JSON.parse(value);
|
|
143
|
-
if (!isPendingTaskShape(parsed.task) || !isResultStatus(parsed.status)) {
|
|
144
|
-
return null;
|
|
145
|
-
}
|
|
146
|
-
return parsed;
|
|
147
|
-
} catch {
|
|
148
|
-
return null;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// src/tab-lock.ts
|
|
153
|
-
async function withTabLock(name, operation) {
|
|
154
|
-
const locks = typeof navigator !== "undefined" ? navigator.locks : void 0;
|
|
155
|
-
if (!locks) {
|
|
156
|
-
return operation();
|
|
157
|
-
}
|
|
158
|
-
return locks.request(name, async () => operation());
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// src/engine.ts
|
|
162
|
-
var DEFAULT_POLL_TICK_MS = 2e3;
|
|
163
|
-
var DEFAULT_POLL_INTERVAL_MS = 1e4;
|
|
164
|
-
var DEFAULT_MAX_FAILURE_COUNT = 5;
|
|
165
|
-
var DEFAULT_RESULT_EVENT = "pending-task-result";
|
|
166
|
-
var DEFAULT_POLL_LEASE_TTL_MULTIPLIER = 4;
|
|
167
|
-
function describeError(error) {
|
|
168
|
-
if (error instanceof Error) return error.message;
|
|
169
|
-
return typeof error === "string" ? error : "Unknown error";
|
|
170
|
-
}
|
|
171
|
-
var PendingTaskPoller = class {
|
|
172
|
-
constructor(options) {
|
|
173
|
-
this.isChecking = false;
|
|
174
|
-
this.pendingForce = false;
|
|
175
|
-
this.stopped = false;
|
|
176
|
-
/** Task ids that already got their one `finalCheckOnExpiry` attempt, so a repeatedly-failing
|
|
177
|
-
* final check doesn't get retried every tick. Reset on process restart — worst case that
|
|
178
|
-
* costs one extra check, never an infinite retry loop.
|
|
179
|
-
*
|
|
180
|
-
* Every id added here (only when a task is expired, right before its final `check()`) is
|
|
181
|
-
* removed again before the *same* tick's iteration moves past that task — either by
|
|
182
|
-
* `finalize()`'s first line, or by one of the leadership-loss/`onCheckError`-intercept
|
|
183
|
-
* branches that bail out without ever reaching `finalize()`. Nothing here is meant to
|
|
184
|
-
* survive past the tick that added it. */
|
|
185
|
-
this.finalCheckAttempted = /* @__PURE__ */ new Set();
|
|
186
|
-
const storageKey = options.storageKey ?? options.store.storageKey ?? DEFAULT_STORAGE_KEY;
|
|
187
|
-
const pollTickMs = options.pollTickMs ?? DEFAULT_POLL_TICK_MS;
|
|
188
|
-
this.options = {
|
|
189
|
-
store: options.store,
|
|
190
|
-
registry: options.registry,
|
|
191
|
-
pollTickMs,
|
|
192
|
-
defaultPollIntervalMs: options.defaultPollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
|
|
193
|
-
defaultTtlMs: options.defaultTtlMs ?? DEFAULT_TTL_MS,
|
|
194
|
-
maxFailureCount: options.maxFailureCount ?? DEFAULT_MAX_FAILURE_COUNT,
|
|
195
|
-
dispatchDomEvent: options.dispatchDomEvent ?? typeof window !== "undefined",
|
|
196
|
-
eventName: options.eventName ?? DEFAULT_RESULT_EVENT,
|
|
197
|
-
storageKey,
|
|
198
|
-
crossTabPollLeaderElection: options.crossTabPollLeaderElection ?? true,
|
|
199
|
-
pollLeaseKey: options.pollLeaseKey ?? `${storageKey}-poll-leader`,
|
|
200
|
-
pollLeaseTtlMs: options.pollLeaseTtlMs ?? pollTickMs * DEFAULT_POLL_LEASE_TTL_MULTIPLIER,
|
|
201
|
-
resultRelayKey: options.resultRelayKey ?? `${storageKey}-result-relay`,
|
|
202
|
-
onResult: options.onResult,
|
|
203
|
-
onCheckError: options.onCheckError,
|
|
204
|
-
claimResultOnce: options.claimResultOnce,
|
|
205
|
-
acceptRelayedResult: options.acceptRelayedResult
|
|
206
|
-
};
|
|
207
|
-
this.ownerId = generatePollOwnerId();
|
|
208
|
-
this.pollLease = createPollLeaseClaimer(this.options.pollLeaseKey, this.options.pollLeaseTtlMs);
|
|
209
|
-
}
|
|
210
|
-
start() {
|
|
211
|
-
this.stopped = false;
|
|
212
|
-
if (this.intervalId !== void 0) return;
|
|
213
|
-
this.intervalId = setInterval(() => {
|
|
214
|
-
this.runTickSafely(false);
|
|
215
|
-
}, this.options.pollTickMs);
|
|
216
|
-
if (typeof window !== "undefined") {
|
|
217
|
-
this.storageListener = (event) => {
|
|
218
|
-
if (event.key === null) {
|
|
219
|
-
this.options.store.writeTasks([]);
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
if (event.key === this.options.storageKey) {
|
|
223
|
-
this.options.store.writeTasks(parseTasksFromStorageValue(event.newValue));
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
if (this.options.crossTabPollLeaderElection && event.key === this.options.resultRelayKey) {
|
|
227
|
-
const detail = parseResultRelay(event.newValue);
|
|
228
|
-
if (!detail) return;
|
|
229
|
-
let accepted;
|
|
230
|
-
try {
|
|
231
|
-
accepted = this.options.acceptRelayedResult?.(detail) ?? true;
|
|
232
|
-
} catch (error) {
|
|
233
|
-
queueMicrotask(() => {
|
|
234
|
-
throw error;
|
|
235
|
-
});
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
if (accepted) void this.dispatchRelayedResult(detail);
|
|
239
|
-
}
|
|
240
|
-
};
|
|
241
|
-
window.addEventListener("storage", this.storageListener);
|
|
242
|
-
}
|
|
243
|
-
this.runTickSafely(false);
|
|
244
|
-
}
|
|
245
|
-
stop() {
|
|
246
|
-
this.stopped = true;
|
|
247
|
-
this.pendingForce = false;
|
|
248
|
-
if (this.intervalId !== void 0) {
|
|
249
|
-
clearInterval(this.intervalId);
|
|
250
|
-
this.intervalId = void 0;
|
|
251
|
-
}
|
|
252
|
-
if (this.storageListener && typeof window !== "undefined") {
|
|
253
|
-
window.removeEventListener("storage", this.storageListener);
|
|
254
|
-
this.storageListener = void 0;
|
|
255
|
-
}
|
|
256
|
-
if (this.options.crossTabPollLeaderElection) {
|
|
257
|
-
void this.releaseLeadership().catch(() => void 0);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
/** Re-check every tracked task right now, bypassing each task's poll interval (e.g. on tab focus). */
|
|
261
|
-
forceCheckAll() {
|
|
262
|
-
this.runTickSafely(true);
|
|
263
|
-
}
|
|
264
|
-
claimLeadership() {
|
|
265
|
-
return withTabLock(this.options.pollLeaseKey, () => this.pollLease.claim(this.ownerId));
|
|
266
|
-
}
|
|
267
|
-
/**
|
|
268
|
-
* Re-confirms that poll leadership is still this tab's — and still the *same continuous
|
|
269
|
-
* tenure* as when `fence` was captured, not just "is nobody else currently holding it" (a
|
|
270
|
-
* no-op returning `fence` unchanged when `crossTabPollLeaderElection` is off). Clears `task`'s
|
|
271
|
-
* `finalCheckAttempted` bookkeeping and returns `false` if not — the caller should stop
|
|
272
|
-
* treating this tick's remaining due tasks as network-eligible (though it may still process
|
|
273
|
-
* ones that need no leadership) rather than act on a possibly-stale outcome.
|
|
274
|
-
*
|
|
275
|
-
* A fence mismatch (rather than just an owner-id mismatch) is needed to catch leadership
|
|
276
|
-
* having churned through another tab and back to this one while a slow `handler.check()` was
|
|
277
|
-
* in flight: this tab's lease can expire mid-check, another tab claims it and fully resolves
|
|
278
|
-
* the same task, and that tab's own lease can *also* expire before this tab's stale response
|
|
279
|
-
* comes back — at which point this tab's next claim legitimately succeeds under its own
|
|
280
|
-
* stable owner id (nothing currently holds the lease), even though leadership genuinely
|
|
281
|
-
* changed hands in between. See `PollLeaseClaimResult`.
|
|
282
|
-
*/
|
|
283
|
-
async reconfirmLeadership(task, expired, fence) {
|
|
284
|
-
if (!this.options.crossTabPollLeaderElection) return fence;
|
|
285
|
-
if (this.stopped) {
|
|
286
|
-
if (expired) this.finalCheckAttempted.delete(task.id);
|
|
287
|
-
return false;
|
|
288
|
-
}
|
|
289
|
-
let result;
|
|
290
|
-
try {
|
|
291
|
-
result = await this.claimLeadership();
|
|
292
|
-
} catch (error) {
|
|
293
|
-
if (expired) this.finalCheckAttempted.delete(task.id);
|
|
294
|
-
throw error;
|
|
295
|
-
}
|
|
296
|
-
if (result.leader && result.fence === fence) return result.fence;
|
|
297
|
-
if (expired) this.finalCheckAttempted.delete(task.id);
|
|
298
|
-
return false;
|
|
299
|
-
}
|
|
300
|
-
releaseLeadership() {
|
|
301
|
-
return withTabLock(this.options.pollLeaseKey, () => {
|
|
302
|
-
this.pollLease.release(this.ownerId);
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
/** Fires `runTick`, but instead of leaving its promise `void`-called (which would turn an
|
|
306
|
-
* exception thrown by a consumer callback — `onResult`, `onCheckError`, or a `dispatchEvent`
|
|
307
|
-
* listener — into a silent unhandled rejection), re-throws it as an uncaught exception on a
|
|
308
|
-
* fresh microtask. `runTick`'s own `finally` has already flushed the batch and reset
|
|
309
|
-
* `isChecking` by the time this ever runs, so a broken consumer callback can't take the
|
|
310
|
-
* poller down — it just becomes visible the way any other uncaught error in the host
|
|
311
|
-
* environment would be, instead of vanishing. */
|
|
312
|
-
runTickSafely(force) {
|
|
313
|
-
this.runTick(force).catch((error) => {
|
|
314
|
-
queueMicrotask(() => {
|
|
315
|
-
throw error;
|
|
316
|
-
});
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
/** Reads the freshest snapshot of `task` from the store, in case another tab wrote to it
|
|
320
|
-
* while this tab's `handler.check()` was in flight — narrows, but doesn't eliminate, the
|
|
321
|
-
* window where a concurrent cross-tab write to the same task could be clobbered.
|
|
322
|
-
*
|
|
323
|
-
* Indexes `store.getState().tasks` into a Map keyed by id rather than doing a linear find
|
|
324
|
-
* each call — this is called once per pending/failing task per tick, so a plain find would
|
|
325
|
-
* make a tick O(n²). The cache keys off the `tasks` array reference, which zustand only
|
|
326
|
-
* replaces on an actual write, so it's rebuilt only when the store has genuinely changed. */
|
|
327
|
-
getLatestTask(task) {
|
|
328
|
-
const tasks = this.options.store.getState().tasks;
|
|
329
|
-
if (this.latestTasksCache?.tasks !== tasks) {
|
|
330
|
-
this.latestTasksCache = { tasks, byId: new Map(tasks.map((t) => [t.id, t])) };
|
|
331
|
-
}
|
|
332
|
-
return this.latestTasksCache.byId.get(task.id) ?? task;
|
|
333
|
-
}
|
|
334
|
-
/** Applies a whole tick's worth of per-task updates/removals (`patch: null` means "remove")
|
|
335
|
-
* in a single read-modify-write, instead of one persisted-storage round trip per task.
|
|
336
|
-
* Reads the freshest persisted list right before writing (same "never resurrect a task
|
|
337
|
-
* another tab already removed" guarantee `PendingTaskStore`'s own mutators give) — unless
|
|
338
|
-
* the store's `hasUnpersistedWrites` is set (a write on *any* path for this store, including
|
|
339
|
-
* this store's own direct mutators, failed and hasn't yet been followed by a success), in
|
|
340
|
-
* which case persisted storage is stale relative to this tab's memory, so this flush builds
|
|
341
|
-
* on `store.getState().tasks` instead. Writes through `writeTasks`, which swallows a
|
|
342
|
-
* throwing write and updates that same shared flag — see the comment in `store.ts`. */
|
|
343
|
-
flushBatch(batch) {
|
|
344
|
-
if (batch.size === 0) return;
|
|
345
|
-
const base = this.options.store.hasUnpersistedWrites ? this.options.store.getState().tasks : readPersistedTasks(this.options.storageKey);
|
|
346
|
-
const next = [];
|
|
347
|
-
for (const t of base) {
|
|
348
|
-
if (!batch.has(t.id)) {
|
|
349
|
-
next.push(t);
|
|
350
|
-
continue;
|
|
351
|
-
}
|
|
352
|
-
const patch = batch.get(t.id);
|
|
353
|
-
if (patch !== null) next.push({ ...t, ...patch });
|
|
354
|
-
}
|
|
355
|
-
this.options.store.writeTasks(next);
|
|
356
|
-
}
|
|
357
|
-
async finalize(task, detail, handler, batch) {
|
|
358
|
-
this.finalCheckAttempted.delete(task.id);
|
|
359
|
-
batch.set(task.id, null);
|
|
360
|
-
if (detail.status === "expired") return;
|
|
361
|
-
const silent = detail.status === "success" ? handler?.silentOnSuccess : handler?.silentOnFailure;
|
|
362
|
-
if (silent) return;
|
|
363
|
-
let claimed;
|
|
364
|
-
try {
|
|
365
|
-
claimed = this.options.claimResultOnce ? await this.options.claimResultOnce(task) : true;
|
|
366
|
-
} catch {
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
if (!claimed) return;
|
|
370
|
-
const fullDetail = { task, ...detail };
|
|
371
|
-
if (this.options.crossTabPollLeaderElection) {
|
|
372
|
-
writeResultRelay(this.options.resultRelayKey, fullDetail);
|
|
373
|
-
}
|
|
374
|
-
this.options.onResult?.(fullDetail);
|
|
375
|
-
if (this.options.dispatchDomEvent && typeof window !== "undefined") {
|
|
376
|
-
window.dispatchEvent(new CustomEvent(this.options.eventName, { detail: fullDetail }));
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
/**
|
|
380
|
-
* Handles a result relayed from another tab's leader — mirrors finalize()'s own
|
|
381
|
-
* `claimResultOnce` gate and dispatch, so a `claimResultOnce` composed for "one notification
|
|
382
|
-
* system-wide" (see its doc comment's "keep both if you want both properties") applies
|
|
383
|
-
* uniformly whether this tab detected the result itself or only learned about it via the
|
|
384
|
-
* relay, not just to the narrower direct-detection race `claimResultOnce` guarded before this
|
|
385
|
-
* relay existed.
|
|
386
|
-
*
|
|
387
|
-
* Fired-and-forgotten (`void`-called) from the "storage" listener rather than awaited, so it
|
|
388
|
-
* has no `this.stopped` check of its own: `stop()` removes the listener (no *new* relayed
|
|
389
|
-
* result starts one of these after that), but one already in flight when `stop()` is called
|
|
390
|
-
* (e.g. awaiting a slow `claimResultOnce`) still runs to completion — the same tolerance
|
|
391
|
-
* `runTick`'s own doc comment describes for an in-flight tick.
|
|
392
|
-
*/
|
|
393
|
-
async dispatchRelayedResult(detail) {
|
|
394
|
-
let claimed;
|
|
395
|
-
try {
|
|
396
|
-
claimed = this.options.claimResultOnce ? await this.options.claimResultOnce(detail.task) : true;
|
|
397
|
-
} catch {
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
if (!claimed) return;
|
|
401
|
-
try {
|
|
402
|
-
this.options.onResult?.(detail);
|
|
403
|
-
if (this.options.dispatchDomEvent && typeof window !== "undefined") {
|
|
404
|
-
window.dispatchEvent(new CustomEvent(this.options.eventName, { detail }));
|
|
405
|
-
}
|
|
406
|
-
} catch (error) {
|
|
407
|
-
queueMicrotask(() => {
|
|
408
|
-
throw error;
|
|
409
|
-
});
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
async runTick(force) {
|
|
413
|
-
if (this.stopped) return;
|
|
414
|
-
if (this.isChecking) {
|
|
415
|
-
this.pendingForce = this.pendingForce || force;
|
|
416
|
-
return;
|
|
417
|
-
}
|
|
418
|
-
const tasks = this.options.store.getState().tasks;
|
|
419
|
-
if (tasks.length === 0) return;
|
|
420
|
-
this.isChecking = true;
|
|
421
|
-
const batch = /* @__PURE__ */ new Map();
|
|
422
|
-
try {
|
|
423
|
-
const now = Date.now();
|
|
424
|
-
let fence;
|
|
425
|
-
let leadershipLost = false;
|
|
426
|
-
for (const task of tasks) {
|
|
427
|
-
const handler = this.options.registry[task.type];
|
|
428
|
-
const ttlMs = task.ttlMs ?? this.options.defaultTtlMs;
|
|
429
|
-
const expired = now - task.startedAt >= ttlMs;
|
|
430
|
-
if (!handler) {
|
|
431
|
-
if (expired) {
|
|
432
|
-
await this.finalize(task, { status: "expired" }, handler, batch);
|
|
433
|
-
}
|
|
434
|
-
continue;
|
|
435
|
-
}
|
|
436
|
-
const interval = handler.pollIntervalMs ?? this.options.defaultPollIntervalMs;
|
|
437
|
-
const lastChecked = task.lastCheckedAt ?? task.startedAt;
|
|
438
|
-
const due = force || now - lastChecked >= interval;
|
|
439
|
-
const finalAttemptDone = this.finalCheckAttempted.has(task.id);
|
|
440
|
-
if (expired && (!handler.finalCheckOnExpiry || finalAttemptDone)) {
|
|
441
|
-
await this.finalize(task, { status: "expired" }, handler, batch);
|
|
442
|
-
continue;
|
|
443
|
-
}
|
|
444
|
-
if (!due && !expired) continue;
|
|
445
|
-
if (expired) {
|
|
446
|
-
this.finalCheckAttempted.add(task.id);
|
|
447
|
-
}
|
|
448
|
-
if (this.options.crossTabPollLeaderElection) {
|
|
449
|
-
if (leadershipLost || this.stopped) {
|
|
450
|
-
leadershipLost = true;
|
|
451
|
-
if (expired) this.finalCheckAttempted.delete(task.id);
|
|
452
|
-
continue;
|
|
453
|
-
}
|
|
454
|
-
if (fence === void 0) {
|
|
455
|
-
let claimed;
|
|
456
|
-
try {
|
|
457
|
-
claimed = await this.claimLeadership();
|
|
458
|
-
} catch (error) {
|
|
459
|
-
if (expired) this.finalCheckAttempted.delete(task.id);
|
|
460
|
-
throw error;
|
|
461
|
-
}
|
|
462
|
-
if (!claimed.leader) {
|
|
463
|
-
leadershipLost = true;
|
|
464
|
-
if (expired) this.finalCheckAttempted.delete(task.id);
|
|
465
|
-
continue;
|
|
466
|
-
}
|
|
467
|
-
fence = claimed.fence;
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
let result;
|
|
471
|
-
try {
|
|
472
|
-
result = await handler.check(task);
|
|
473
|
-
} catch (error) {
|
|
474
|
-
const reconfirmedOnError = await this.reconfirmLeadership(task, expired, fence);
|
|
475
|
-
if (reconfirmedOnError === false) {
|
|
476
|
-
leadershipLost = true;
|
|
477
|
-
fence = void 0;
|
|
478
|
-
continue;
|
|
479
|
-
}
|
|
480
|
-
fence = reconfirmedOnError;
|
|
481
|
-
let intercepted;
|
|
482
|
-
try {
|
|
483
|
-
intercepted = this.options.onCheckError?.(error, task);
|
|
484
|
-
} catch (onCheckErrorError) {
|
|
485
|
-
queueMicrotask(() => {
|
|
486
|
-
throw onCheckErrorError;
|
|
487
|
-
});
|
|
488
|
-
intercepted = false;
|
|
489
|
-
}
|
|
490
|
-
if (intercepted) {
|
|
491
|
-
batch.set(task.id, { lastCheckedAt: now });
|
|
492
|
-
this.finalCheckAttempted.delete(task.id);
|
|
493
|
-
break;
|
|
494
|
-
}
|
|
495
|
-
if (expired) {
|
|
496
|
-
await this.finalize(task, { status: "expired" }, handler, batch);
|
|
497
|
-
continue;
|
|
498
|
-
}
|
|
499
|
-
const latest = this.getLatestTask(task);
|
|
500
|
-
const failureCount = (latest.failureCount ?? 0) + 1;
|
|
501
|
-
if (failureCount >= this.options.maxFailureCount) {
|
|
502
|
-
await this.finalize(task, { status: "error", data: describeError(error) }, handler, batch);
|
|
503
|
-
} else {
|
|
504
|
-
batch.set(task.id, { lastCheckedAt: now, failureCount });
|
|
505
|
-
}
|
|
506
|
-
continue;
|
|
507
|
-
}
|
|
508
|
-
const reconfirmedOnSuccess = await this.reconfirmLeadership(task, expired, fence);
|
|
509
|
-
if (reconfirmedOnSuccess === false) {
|
|
510
|
-
leadershipLost = true;
|
|
511
|
-
fence = void 0;
|
|
512
|
-
continue;
|
|
513
|
-
}
|
|
514
|
-
fence = reconfirmedOnSuccess;
|
|
515
|
-
if (result.status === "pending") {
|
|
516
|
-
const latest = this.getLatestTask(task);
|
|
517
|
-
batch.set(task.id, {
|
|
518
|
-
lastCheckedAt: now,
|
|
519
|
-
failureCount: 0,
|
|
520
|
-
metadata: { ...latest.metadata, ...result.progress }
|
|
521
|
-
});
|
|
522
|
-
if (expired) {
|
|
523
|
-
await this.finalize(task, { status: "expired" }, handler, batch);
|
|
524
|
-
}
|
|
525
|
-
continue;
|
|
526
|
-
}
|
|
527
|
-
await this.finalize(task, { status: result.status, data: result.data }, handler, batch);
|
|
528
|
-
}
|
|
529
|
-
} finally {
|
|
530
|
-
this.flushBatch(batch);
|
|
531
|
-
this.isChecking = false;
|
|
532
|
-
if (this.pendingForce && !this.stopped) {
|
|
533
|
-
this.pendingForce = false;
|
|
534
|
-
this.runTickSafely(true);
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
};
|
|
539
|
-
|
|
540
|
-
// src/react.ts
|
|
541
|
-
function usePendingTaskPoller(options) {
|
|
542
|
-
const { enabled = true, ...pollerOptions } = options;
|
|
543
|
-
const optionsRef = (0, import_react.useRef)(pollerOptions);
|
|
544
|
-
optionsRef.current = pollerOptions;
|
|
545
|
-
(0, import_react.useEffect)(() => {
|
|
546
|
-
if (!enabled) return;
|
|
547
|
-
const poller = new PendingTaskPoller({
|
|
548
|
-
...optionsRef.current,
|
|
549
|
-
onResult: (detail) => optionsRef.current.onResult?.(detail),
|
|
550
|
-
onCheckError: (error, task) => optionsRef.current.onCheckError?.(error, task),
|
|
551
|
-
claimResultOnce: (task) => optionsRef.current.claimResultOnce ? optionsRef.current.claimResultOnce(task) : true,
|
|
552
|
-
acceptRelayedResult: (detail) => optionsRef.current.acceptRelayedResult?.(detail) ?? true
|
|
553
|
-
});
|
|
554
|
-
poller.start();
|
|
555
|
-
const handleVisibility = () => {
|
|
556
|
-
if (document.visibilityState === "visible") {
|
|
557
|
-
poller.forceCheckAll();
|
|
558
|
-
}
|
|
559
|
-
};
|
|
560
|
-
document.addEventListener("visibilitychange", handleVisibility);
|
|
561
|
-
return () => {
|
|
562
|
-
document.removeEventListener("visibilitychange", handleVisibility);
|
|
563
|
-
poller.stop();
|
|
564
|
-
};
|
|
565
|
-
}, [enabled, pollerOptions.store, pollerOptions.registry]);
|
|
566
|
-
}
|
|
567
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
568
|
-
0 && (module.exports = {
|
|
569
|
-
PendingTaskPoller,
|
|
570
|
-
usePendingTaskPoller
|
|
571
|
-
});
|
|
572
|
-
//# sourceMappingURL=react.cjs.map
|