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.js
CHANGED
|
@@ -90,8 +90,7 @@ function parseTasksFromStorageValue(value) {
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
function readPersistedTasks(storageKey) {
|
|
93
|
-
|
|
94
|
-
return parseTasksFromStorageValue(localStorage.getItem(storageKey));
|
|
93
|
+
return parseTasksFromStorageValue(safeGetItem(storageKey));
|
|
95
94
|
}
|
|
96
95
|
|
|
97
96
|
// src/result-relay.ts
|
|
@@ -148,6 +147,10 @@ var PendingTaskPoller = class {
|
|
|
148
147
|
this.isChecking = false;
|
|
149
148
|
this.pendingForce = false;
|
|
150
149
|
this.stopped = false;
|
|
150
|
+
/** This tab's own most recently reported leadership status, for `onLeaderChange` — tracked
|
|
151
|
+
* here (rather than derived fresh each time from `fence`) purely so that callback fires only
|
|
152
|
+
* on an actual flip, not once per tick it happens to still hold/still lack leadership. */
|
|
153
|
+
this.isLeaderTab = false;
|
|
151
154
|
/** Task ids that already got their one `finalCheckOnExpiry` attempt, so a repeatedly-failing
|
|
152
155
|
* final check doesn't get retried every tick. Reset on process restart — worst case that
|
|
153
156
|
* costs one extra check, never an infinite retry loop.
|
|
@@ -177,7 +180,9 @@ var PendingTaskPoller = class {
|
|
|
177
180
|
onResult: options.onResult,
|
|
178
181
|
onCheckError: options.onCheckError,
|
|
179
182
|
claimResultOnce: options.claimResultOnce,
|
|
180
|
-
acceptRelayedResult: options.acceptRelayedResult
|
|
183
|
+
acceptRelayedResult: options.acceptRelayedResult,
|
|
184
|
+
onLeaderChange: options.onLeaderChange,
|
|
185
|
+
onTick: options.onTick
|
|
181
186
|
};
|
|
182
187
|
this.ownerId = generatePollOwnerId();
|
|
183
188
|
this.pollLease = createPollLeaseClaimer(this.options.pollLeaseKey, this.options.pollLeaseTtlMs);
|
|
@@ -220,6 +225,7 @@ var PendingTaskPoller = class {
|
|
|
220
225
|
stop() {
|
|
221
226
|
this.stopped = true;
|
|
222
227
|
this.pendingForce = false;
|
|
228
|
+
this.inFlightAbortController?.abort();
|
|
223
229
|
if (this.intervalId !== void 0) {
|
|
224
230
|
clearInterval(this.intervalId);
|
|
225
231
|
this.intervalId = void 0;
|
|
@@ -230,6 +236,7 @@ var PendingTaskPoller = class {
|
|
|
230
236
|
}
|
|
231
237
|
if (this.options.crossTabPollLeaderElection) {
|
|
232
238
|
void this.releaseLeadership().catch(() => void 0);
|
|
239
|
+
this.setLeaderStatus(false);
|
|
233
240
|
}
|
|
234
241
|
}
|
|
235
242
|
/** Re-check every tracked task right now, bypassing each task's poll interval (e.g. on tab focus). */
|
|
@@ -239,6 +246,25 @@ var PendingTaskPoller = class {
|
|
|
239
246
|
claimLeadership() {
|
|
240
247
|
return withTabLock(this.options.pollLeaseKey, () => this.pollLease.claim(this.ownerId));
|
|
241
248
|
}
|
|
249
|
+
/** Updates `isLeaderTab` and fires `onLeaderChange`, but only on an actual flip — see that
|
|
250
|
+
* option's doc comment, including the "only ever called when `crossTabPollLeaderElection` is
|
|
251
|
+
* on" part, which this enforces itself rather than relying on every call site to remember to
|
|
252
|
+
* guard it (a call site that forgot would otherwise be a real, undetected bug — the whole
|
|
253
|
+
* reason this guard lives here instead of at each of this method's several call sites). Safe
|
|
254
|
+
* to call redundantly (e.g. after every successful claim/reconfirm in a tick, not just the
|
|
255
|
+
* first) since a no-op call is just an equality check. */
|
|
256
|
+
setLeaderStatus(isLeader) {
|
|
257
|
+
if (!this.options.crossTabPollLeaderElection) return;
|
|
258
|
+
if (this.isLeaderTab === isLeader) return;
|
|
259
|
+
this.isLeaderTab = isLeader;
|
|
260
|
+
try {
|
|
261
|
+
this.options.onLeaderChange?.(isLeader);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
queueMicrotask(() => {
|
|
264
|
+
throw error;
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
242
268
|
/**
|
|
243
269
|
* Re-confirms that poll leadership is still this tab's — and still the *same continuous
|
|
244
270
|
* tenure* as when `fence` was captured, not just "is nobody else currently holding it" (a
|
|
@@ -256,11 +282,11 @@ var PendingTaskPoller = class {
|
|
|
256
282
|
* changed hands in between. See `PollLeaseClaimResult`.
|
|
257
283
|
*/
|
|
258
284
|
async reconfirmLeadership(task, expired, fence) {
|
|
259
|
-
if (!this.options.crossTabPollLeaderElection) return fence;
|
|
260
285
|
if (this.stopped) {
|
|
261
286
|
if (expired) this.finalCheckAttempted.delete(task.id);
|
|
262
287
|
return false;
|
|
263
288
|
}
|
|
289
|
+
if (!this.options.crossTabPollLeaderElection) return fence;
|
|
264
290
|
let result;
|
|
265
291
|
try {
|
|
266
292
|
result = await this.claimLeadership();
|
|
@@ -394,8 +420,8 @@ var PendingTaskPoller = class {
|
|
|
394
420
|
if (tasks.length === 0) return;
|
|
395
421
|
this.isChecking = true;
|
|
396
422
|
const batch = /* @__PURE__ */ new Map();
|
|
423
|
+
const now = Date.now();
|
|
397
424
|
try {
|
|
398
|
-
const now = Date.now();
|
|
399
425
|
let fence;
|
|
400
426
|
let leadershipLost = false;
|
|
401
427
|
for (const task of tasks) {
|
|
@@ -408,7 +434,19 @@ var PendingTaskPoller = class {
|
|
|
408
434
|
}
|
|
409
435
|
continue;
|
|
410
436
|
}
|
|
411
|
-
const
|
|
437
|
+
const failureCount = task.failureCount ?? 0;
|
|
438
|
+
let backoffMs;
|
|
439
|
+
if (failureCount > 0) {
|
|
440
|
+
try {
|
|
441
|
+
backoffMs = handler.retryBackoffMs?.(failureCount);
|
|
442
|
+
} catch (retryBackoffMsError) {
|
|
443
|
+
queueMicrotask(() => {
|
|
444
|
+
throw retryBackoffMsError;
|
|
445
|
+
});
|
|
446
|
+
backoffMs = void 0;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
const interval = backoffMs !== void 0 && Number.isFinite(backoffMs) && backoffMs > 0 ? backoffMs : handler.pollIntervalMs ?? this.options.defaultPollIntervalMs;
|
|
412
450
|
const lastChecked = task.lastCheckedAt ?? task.startedAt;
|
|
413
451
|
const due = force || now - lastChecked >= interval;
|
|
414
452
|
const finalAttemptDone = this.finalCheckAttempted.has(task.id);
|
|
@@ -434,25 +472,31 @@ var PendingTaskPoller = class {
|
|
|
434
472
|
if (expired) this.finalCheckAttempted.delete(task.id);
|
|
435
473
|
throw error;
|
|
436
474
|
}
|
|
437
|
-
if (!claimed.leader) {
|
|
475
|
+
if (this.stopped || !claimed.leader) {
|
|
438
476
|
leadershipLost = true;
|
|
477
|
+
this.setLeaderStatus(false);
|
|
439
478
|
if (expired) this.finalCheckAttempted.delete(task.id);
|
|
440
479
|
continue;
|
|
441
480
|
}
|
|
442
481
|
fence = claimed.fence;
|
|
482
|
+
this.setLeaderStatus(true);
|
|
443
483
|
}
|
|
444
484
|
}
|
|
445
485
|
let result;
|
|
486
|
+
const abortController = new AbortController();
|
|
487
|
+
this.inFlightAbortController = abortController;
|
|
446
488
|
try {
|
|
447
|
-
result = await handler.check(task);
|
|
489
|
+
result = await handler.check(task, abortController.signal);
|
|
448
490
|
} catch (error) {
|
|
449
491
|
const reconfirmedOnError = await this.reconfirmLeadership(task, expired, fence);
|
|
450
492
|
if (reconfirmedOnError === false) {
|
|
451
493
|
leadershipLost = true;
|
|
452
494
|
fence = void 0;
|
|
495
|
+
this.setLeaderStatus(false);
|
|
453
496
|
continue;
|
|
454
497
|
}
|
|
455
498
|
fence = reconfirmedOnError;
|
|
499
|
+
this.setLeaderStatus(true);
|
|
456
500
|
let intercepted;
|
|
457
501
|
try {
|
|
458
502
|
intercepted = this.options.onCheckError?.(error, task);
|
|
@@ -472,21 +516,25 @@ var PendingTaskPoller = class {
|
|
|
472
516
|
continue;
|
|
473
517
|
}
|
|
474
518
|
const latest = this.getLatestTask(task);
|
|
475
|
-
const
|
|
476
|
-
if (
|
|
519
|
+
const failureCount2 = (latest.failureCount ?? 0) + 1;
|
|
520
|
+
if (failureCount2 >= this.options.maxFailureCount) {
|
|
477
521
|
await this.finalize(task, { status: "error", data: describeError(error) }, handler, batch);
|
|
478
522
|
} else {
|
|
479
|
-
batch.set(task.id, { lastCheckedAt: now, failureCount });
|
|
523
|
+
batch.set(task.id, { lastCheckedAt: now, failureCount: failureCount2 });
|
|
480
524
|
}
|
|
481
525
|
continue;
|
|
526
|
+
} finally {
|
|
527
|
+
if (this.inFlightAbortController === abortController) this.inFlightAbortController = void 0;
|
|
482
528
|
}
|
|
483
529
|
const reconfirmedOnSuccess = await this.reconfirmLeadership(task, expired, fence);
|
|
484
530
|
if (reconfirmedOnSuccess === false) {
|
|
485
531
|
leadershipLost = true;
|
|
486
532
|
fence = void 0;
|
|
533
|
+
this.setLeaderStatus(false);
|
|
487
534
|
continue;
|
|
488
535
|
}
|
|
489
536
|
fence = reconfirmedOnSuccess;
|
|
537
|
+
this.setLeaderStatus(true);
|
|
490
538
|
if (result.status === "pending") {
|
|
491
539
|
const latest = this.getLatestTask(task);
|
|
492
540
|
batch.set(task.id, {
|
|
@@ -502,8 +550,23 @@ var PendingTaskPoller = class {
|
|
|
502
550
|
await this.finalize(task, { status: result.status, data: result.data }, handler, batch);
|
|
503
551
|
}
|
|
504
552
|
} finally {
|
|
505
|
-
this.flushBatch(batch);
|
|
506
553
|
this.isChecking = false;
|
|
554
|
+
try {
|
|
555
|
+
this.flushBatch(batch);
|
|
556
|
+
} catch (error) {
|
|
557
|
+
queueMicrotask(() => {
|
|
558
|
+
throw error;
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
if (this.options.onTick) {
|
|
562
|
+
try {
|
|
563
|
+
this.options.onTick({ durationMs: Date.now() - now, taskCount: tasks.length });
|
|
564
|
+
} catch (error) {
|
|
565
|
+
queueMicrotask(() => {
|
|
566
|
+
throw error;
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
}
|
|
507
570
|
if (this.pendingForce && !this.stopped) {
|
|
508
571
|
this.pendingForce = false;
|
|
509
572
|
this.runTickSafely(true);
|
|
@@ -524,7 +587,9 @@ function usePendingTaskPoller(options) {
|
|
|
524
587
|
onResult: (detail) => optionsRef.current.onResult?.(detail),
|
|
525
588
|
onCheckError: (error, task) => optionsRef.current.onCheckError?.(error, task),
|
|
526
589
|
claimResultOnce: (task) => optionsRef.current.claimResultOnce ? optionsRef.current.claimResultOnce(task) : true,
|
|
527
|
-
acceptRelayedResult: (detail) => optionsRef.current.acceptRelayedResult?.(detail) ?? true
|
|
590
|
+
acceptRelayedResult: (detail) => optionsRef.current.acceptRelayedResult?.(detail) ?? true,
|
|
591
|
+
onLeaderChange: (isLeader) => optionsRef.current.onLeaderChange?.(isLeader),
|
|
592
|
+
onTick: (info) => optionsRef.current.onTick?.(info)
|
|
528
593
|
});
|
|
529
594
|
poller.start();
|
|
530
595
|
const handleVisibility = () => {
|