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
|
@@ -1,362 +0,0 @@
|
|
|
1
|
-
import { UseBoundStore, StoreApi } from 'zustand';
|
|
2
|
-
|
|
3
|
-
/** What `handler.check()` itself reports about the task on a given poll: still going, or
|
|
4
|
-
* finished — with the finished outcome being a business-level judgment (`success`/`failure`)
|
|
5
|
-
* the handler makes, not something the engine interprets. */
|
|
6
|
-
type PendingTaskStatus = "pending" | "success" | "failure";
|
|
7
|
-
/**
|
|
8
|
-
* The final way a task's tracking concluded, as reported to `onResult`:
|
|
9
|
-
* - `success`/`failure` — `check()` gave a definite answer (mirrors `PendingTaskStatus`).
|
|
10
|
-
* - `error` — `check()` itself kept throwing until `maxFailureCount` was reached; the engine
|
|
11
|
-
* never learned whether the task actually succeeded or failed.
|
|
12
|
-
* - `expired` — the task's TTL ran out before a definite answer arrived; same "never learned
|
|
13
|
-
* the outcome" situation as `error`, just timed out rather than failing outright.
|
|
14
|
-
*/
|
|
15
|
-
type PendingTaskResultStatus = "success" | "failure" | "error" | "expired";
|
|
16
|
-
/**
|
|
17
|
-
* Free-form bag for whatever your app wants attached to a task — a display title, a link, an
|
|
18
|
-
* owning user/tenant id, whatever `check()` or your UI needs. Entirely yours: the engine never
|
|
19
|
-
* reads or writes anything in here (see `PendingTask.failureCount`/`ttlMs` for the fields it
|
|
20
|
-
* *does* maintain itself — kept as separate top-level fields precisely so they can never
|
|
21
|
-
* collide with a key you pick here).
|
|
22
|
-
*/
|
|
23
|
-
type PendingTaskMetadata = Record<string, unknown>;
|
|
24
|
-
interface PendingTask<TType extends string = string> {
|
|
25
|
-
/** Stable, globally-unique id. Re-adding a task with the same id replaces it. */
|
|
26
|
-
id: string;
|
|
27
|
-
type: TType;
|
|
28
|
-
taskId: number | string;
|
|
29
|
-
startedAt: number;
|
|
30
|
-
lastCheckedAt?: number;
|
|
31
|
-
/** Consecutive check-failure count; maintained by the poller, not by handlers. */
|
|
32
|
-
failureCount?: number;
|
|
33
|
-
/** TTL (ms) frozen onto the task at creation time — see `PendingTaskRegistry.addTask`. */
|
|
34
|
-
ttlMs?: number;
|
|
35
|
-
metadata?: PendingTaskMetadata;
|
|
36
|
-
}
|
|
37
|
-
interface PendingTaskCheckResult {
|
|
38
|
-
status: PendingTaskStatus;
|
|
39
|
-
/** Merged into `task.metadata` on the next store update — a percent, a stage name,
|
|
40
|
-
* a step count, or whatever shape your handler's progress reporting needs. */
|
|
41
|
-
progress?: Record<string, unknown>;
|
|
42
|
-
/**
|
|
43
|
-
* Free-form payload for the caller's own `onResult` handling — a link, a toast message,
|
|
44
|
-
* an action-button label, what cache to invalidate, or anything else. The engine has no
|
|
45
|
-
* opinion on shape or on what a "result" should look like; it only ever passes this through.
|
|
46
|
-
*/
|
|
47
|
-
data?: unknown;
|
|
48
|
-
}
|
|
49
|
-
interface PendingTaskHandler<TType extends string = string> {
|
|
50
|
-
check: (task: PendingTask<TType>) => Promise<PendingTaskCheckResult>;
|
|
51
|
-
/** How often (ms) this task type is checked. Defaults to the poller's `defaultPollIntervalMs`. */
|
|
52
|
-
pollIntervalMs?: number;
|
|
53
|
-
/** How long (ms) an untracked-to-completion task is kept before being dropped. */
|
|
54
|
-
ttlMs?: number;
|
|
55
|
-
/** Force one last `check()` exactly at TTL expiry instead of silently dropping the task. */
|
|
56
|
-
finalCheckOnExpiry?: boolean;
|
|
57
|
-
/** Suppress `onResult` for a `failure` or `error` outcome (still resolved internally).
|
|
58
|
-
* `expired` is always silent regardless of this flag — see `PendingTaskResultStatus`. */
|
|
59
|
-
silentOnFailure?: boolean;
|
|
60
|
-
/** Suppress `onResult` for a `success` outcome. */
|
|
61
|
-
silentOnSuccess?: boolean;
|
|
62
|
-
}
|
|
63
|
-
type PendingTaskRegistry<TType extends string = string> = Partial<Record<TType, PendingTaskHandler<TType>>>;
|
|
64
|
-
interface PendingTaskResultEventDetail<TType extends string = string> {
|
|
65
|
-
task: PendingTask<TType>;
|
|
66
|
-
status: PendingTaskResultStatus;
|
|
67
|
-
data?: unknown;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
declare const DEFAULT_TTL_MS: number;
|
|
71
|
-
declare const DEFAULT_STORAGE_KEY = "pending-tasks";
|
|
72
|
-
interface PendingTaskStoreState<TType extends string = string> {
|
|
73
|
-
tasks: PendingTask<TType>[];
|
|
74
|
-
addTask: (task: PendingTask<TType>) => void;
|
|
75
|
-
removeTask: (id: string) => void;
|
|
76
|
-
updateTask: (id: string, patch: Partial<PendingTask<TType>>) => void;
|
|
77
|
-
/** Removes every task for which `predicate` returns false — e.g. drop tasks that don't
|
|
78
|
-
* belong to the account now signed in, however your app identifies "belongs to". */
|
|
79
|
-
pruneTasksBy: (predicate: (task: PendingTask<TType>) => boolean) => void;
|
|
80
|
-
clearAllTasks: () => void;
|
|
81
|
-
}
|
|
82
|
-
type PendingTaskStore<TType extends string = string> = UseBoundStore<StoreApi<PendingTaskStoreState<TType>>> & {
|
|
83
|
-
storageKey: string;
|
|
84
|
-
/**
|
|
85
|
-
* True when the most recent write to this store's localStorage entry threw (quota exceeded,
|
|
86
|
-
* Safari private browsing, storage disabled, ...) instead of landing — by whichever writer
|
|
87
|
-
* made it: this store's own mutators, or an external batched write via `writeTasks` (e.g.
|
|
88
|
-
* `PendingTaskPoller`'s `flushBatch`/cross-tab `storage` sync). While true, persisted storage
|
|
89
|
-
* no longer reflects this tab's in-memory state, so anything about to rebuild `tasks` from a
|
|
90
|
-
* freshly-read persisted snapshot should build on `getState().tasks` instead, and anything
|
|
91
|
-
* checking "does another tab already have this task" (e.g. `addTaskIfMissing`) should not
|
|
92
|
-
* trust a persisted-storage read either. Flips back to `false` as soon as a write through
|
|
93
|
-
* this store's own mutators or `writeTasks` succeeds again.
|
|
94
|
-
*
|
|
95
|
-
* This is the single shared source of truth for that fact — treat it as read-only from
|
|
96
|
-
* outside this module; it's written only by this store's own mutators and by `writeTasks`.
|
|
97
|
-
*/
|
|
98
|
-
hasUnpersistedWrites: boolean;
|
|
99
|
-
/**
|
|
100
|
-
* Writes `tasks` to this store the same way its own mutators do, through the single shared
|
|
101
|
-
* safe-write path that also updates `hasUnpersistedWrites`. Anything outside this module that
|
|
102
|
-
* replaces the whole `tasks` array wholesale (currently `PendingTaskPoller`'s batched
|
|
103
|
-
* `flushBatch` writes and its cross-tab `storage`-event sync) must go through this instead of
|
|
104
|
-
* calling `setState` directly — otherwise its own write failures would be invisible to this
|
|
105
|
-
* store's mutators (and vice versa), letting the two silently drift out of sync about whether
|
|
106
|
-
* persisted storage can currently be trusted.
|
|
107
|
-
*/
|
|
108
|
-
writeTasks: (tasks: PendingTask<TType>[]) => void;
|
|
109
|
-
};
|
|
110
|
-
interface CreatePendingTaskStoreOptions {
|
|
111
|
-
/** localStorage key. Defaults to `"pending-tasks"`. Must be unique per app if you run multiple stores. */
|
|
112
|
-
storageKey?: string;
|
|
113
|
-
}
|
|
114
|
-
declare function isPendingTaskShape(value: unknown): value is PendingTask;
|
|
115
|
-
/** Parses the raw string a zustand-persist localStorage entry holds, tolerating garbage/foreign values. */
|
|
116
|
-
declare function parseTasksFromStorageValue<TType extends string = string>(value: string | null): PendingTask<TType>[];
|
|
117
|
-
/**
|
|
118
|
-
* Creates an isolated pending-task store. Each store persists to its own localStorage key,
|
|
119
|
-
* so most apps should create exactly one instance and share it (module-level singleton).
|
|
120
|
-
*
|
|
121
|
-
* Every mutator re-reads the persisted value before writing, rather than trusting the
|
|
122
|
-
* in-memory snapshot — this avoids resurrecting a task another tab already removed.
|
|
123
|
-
*/
|
|
124
|
-
declare function createPendingTaskStore<TType extends string = string>(options?: CreatePendingTaskStoreOptions): PendingTaskStore<TType>;
|
|
125
|
-
|
|
126
|
-
declare const DEFAULT_POLL_TICK_MS = 2000;
|
|
127
|
-
declare const DEFAULT_POLL_INTERVAL_MS = 10000;
|
|
128
|
-
declare const DEFAULT_MAX_FAILURE_COUNT = 5;
|
|
129
|
-
declare const DEFAULT_RESULT_EVENT = "pending-task-result";
|
|
130
|
-
/** Multiplied by the effective `pollTickMs` to get the default `pollLeaseTtlMs` — see that
|
|
131
|
-
* option's doc comment for why it needs headroom over a single tick. */
|
|
132
|
-
declare const DEFAULT_POLL_LEASE_TTL_MULTIPLIER = 4;
|
|
133
|
-
interface PendingTaskPollerOptions<TType extends string = string> {
|
|
134
|
-
store: PendingTaskStore<TType>;
|
|
135
|
-
registry: PendingTaskRegistry<TType>;
|
|
136
|
-
/** Called for every non-silent `success`/`failure`/`error` outcome. This is where apps show a toast, navigate, or invalidate a cache — the engine has no opinion on any of that. */
|
|
137
|
-
onResult?: (detail: PendingTaskResultEventDetail<TType>) => void;
|
|
138
|
-
/**
|
|
139
|
-
* Called whenever `handler.check` throws, before the normal failure-count/backoff/expiry
|
|
140
|
-
* handling runs. The engine has no notion of auth, tokens, or sessions — if `check()` can
|
|
141
|
-
* fail for a reason that shouldn't count as a normal transient error (e.g. the caller's own
|
|
142
|
-
* session just ended), inspect `error` here and return `true` to skip the normal failure
|
|
143
|
-
* counting and stop the *current* tick early (the task is left as-is, `lastCheckedAt` is
|
|
144
|
-
* still bumped so it isn't treated as overdue again immediately). Return `false`/`undefined`
|
|
145
|
-
* (or omit this option) to fall through to the standard failure-count/backoff/expiry path.
|
|
146
|
-
*/
|
|
147
|
-
onCheckError?: (error: unknown, task: PendingTask<TType>) => boolean | void;
|
|
148
|
-
/**
|
|
149
|
-
* Optional cross-tab "claim once" gate around the final `onResult`/DOM-event dispatch
|
|
150
|
-
* (task removal from the store always happens regardless). Compose `withTabLock` +
|
|
151
|
-
* `createTtlDedupeCache` here to prevent duplicate toasts when multiple tabs race to
|
|
152
|
-
* process the same completed task. This guards `onResult`/the toast-equivalent side effect
|
|
153
|
-
* specifically — it's orthogonal to `crossTabPollLeaderElection`, which is about not
|
|
154
|
-
* duplicating the *polling* itself; keep both if you want both properties.
|
|
155
|
-
*
|
|
156
|
-
* If your own implementation layers in something time-sensitive of its own — e.g. only
|
|
157
|
-
* proceeding while a session is still valid — check that condition *after* your `withTabLock`
|
|
158
|
-
* call resolves, not only before it: `withTabLock` is a genuine async yield (real cross-tab
|
|
159
|
-
* lock arbitration), so state can legitimately change while it's pending. A check placed only
|
|
160
|
-
* before it can pass, then have the underlying condition change during the wait, and the
|
|
161
|
-
* claimed/dispatched side effect would still fire against the now-stale state. (`finalize()`
|
|
162
|
-
* itself calls this once and acts on the result immediately after, with no further `await` in
|
|
163
|
-
* between — the same "recheck right before acting" discipline applies to whatever you put
|
|
164
|
-
* inside this callback.)
|
|
165
|
-
*
|
|
166
|
-
* On the leader tab specifically, returning `false` here also suppresses the cross-tab result
|
|
167
|
-
* relay (see `resultRelayKey`) for this result — not just this tab's own `onResult`/DOM event.
|
|
168
|
-
* That's the right call for the dedup use case above (another tab already claimed it, so that
|
|
169
|
-
* other tab is the one that will relay). If you layer in a veto unrelated to dedup (the
|
|
170
|
-
* session-validity check above, evaluated on the *leader's* state), a `false` there means
|
|
171
|
-
* *every* open tab loses this result, including ones whose own session is still fine — for a
|
|
172
|
-
* receiving-side-only veto that only affects the tab evaluating it, use `acceptRelayedResult`
|
|
173
|
-
* instead (or alongside this).
|
|
174
|
-
*/
|
|
175
|
-
claimResultOnce?: (task: PendingTask<TType>) => Promise<boolean> | boolean;
|
|
176
|
-
/** How often the engine re-scans the task list. Individual tasks still respect their own poll interval. */
|
|
177
|
-
pollTickMs?: number;
|
|
178
|
-
/** Fallback per-check interval (ms) for handlers that don't set `pollIntervalMs`. */
|
|
179
|
-
defaultPollIntervalMs?: number;
|
|
180
|
-
/** Fallback TTL (ms) for tasks whose handler doesn't set `ttlMs`. */
|
|
181
|
-
defaultTtlMs?: number;
|
|
182
|
-
maxFailureCount?: number;
|
|
183
|
-
/** Also `window.dispatchEvent(new CustomEvent(eventName, { detail }))` for cross-component listening. Defaults to true when `window` exists. */
|
|
184
|
-
dispatchDomEvent?: boolean;
|
|
185
|
-
eventName?: string;
|
|
186
|
-
/** localStorage key the store persists to — must match what `createPendingTaskStore` was given. */
|
|
187
|
-
storageKey?: string;
|
|
188
|
-
/**
|
|
189
|
-
* When multiple browser tabs share the same store (the normal case — the store already
|
|
190
|
-
* syncs across tabs via `storage` events), only one of them actually calls `handler.check()`
|
|
191
|
-
* for a given task at a time; the others skip their own network work entirely for tasks
|
|
192
|
-
* that tab isn't the elected leader for, and instead learn the outcome via the (also
|
|
193
|
-
* newly-enabled) result relay once the leader dispatches it — see `resultRelayKey`.
|
|
194
|
-
*
|
|
195
|
-
* Defaults to `true`. Safe to leave on for single-tab usage: an uncontested instance always
|
|
196
|
-
* successfully claims/renews its own lease, so this changes nothing when there's no
|
|
197
|
-
* contention. Turn it off only if you specifically don't want that (e.g. you're not running
|
|
198
|
-
* in an environment with shared `localStorage` across the "tabs" this is designed for, or
|
|
199
|
-
* you're intentionally running independent pollers that must each poll everything).
|
|
200
|
-
*/
|
|
201
|
-
crossTabPollLeaderElection?: boolean;
|
|
202
|
-
/** localStorage key (and Web Lock name) backing the poll-leader lease. Defaults to
|
|
203
|
-
* `` `${storageKey}-poll-leader` ``. Only relevant when `crossTabPollLeaderElection` is on. */
|
|
204
|
-
pollLeaseKey?: string;
|
|
205
|
-
/**
|
|
206
|
-
* How long a claimed poll-leader lease stays valid without renewal before another tab may
|
|
207
|
-
* claim it. Defaults to `pollTickMs * 4` — comfortably longer than one normal tick, so a
|
|
208
|
-
* live leader always renews well before expiry, but short enough that a leader that stops
|
|
209
|
-
* renewing (closed, crashed, or frozen in the browser's back/forward cache) only blocks
|
|
210
|
-
* takeover for a bounded, short window rather than indefinitely.
|
|
211
|
-
*
|
|
212
|
-
* Note this bounds *renewal cadence*, not any single `handler.check()` call's own duration:
|
|
213
|
-
* a single slow request can still outlast this TTL, which is exactly why the engine
|
|
214
|
-
* re-confirms leadership again right after `check()` resolves/throws, before acting on a
|
|
215
|
-
* possibly-stale outcome — see the source of `runTick` if you're curious about the mechanism.
|
|
216
|
-
* Raising this value doesn't need to account for that case; it only trades off how long a
|
|
217
|
-
* genuinely dead leader blocks takeover.
|
|
218
|
-
*/
|
|
219
|
-
pollLeaseTtlMs?: number;
|
|
220
|
-
/** localStorage key used to relay a completed task's result to other tabs when
|
|
221
|
-
* `crossTabPollLeaderElection` is on (only the leader tab detects completion, so without
|
|
222
|
-
* this, every other tab's `dispatchDomEvent` listeners would never fire). Defaults to
|
|
223
|
-
* `` `${storageKey}-result-relay` ``. */
|
|
224
|
-
resultRelayKey?: string;
|
|
225
|
-
/**
|
|
226
|
-
* Optional gate on the *receiving* side of the cross-tab result relay (only relevant when
|
|
227
|
-
* `crossTabPollLeaderElection` is on): called right before this tab re-dispatches a result
|
|
228
|
-
* that arrived via another tab's `storage` write, letting this tab veto it. Return `false`
|
|
229
|
-
* to skip the dispatch entirely. Omit it (the default) to always accept, matching the
|
|
230
|
-
* engine's behavior before this option existed.
|
|
231
|
-
*
|
|
232
|
-
* The engine has no notion of sessions — if a relayed result could belong to a session that
|
|
233
|
-
* has since ended in *this* tab (a different account signed in, a logout), and re-surfacing
|
|
234
|
-
* it to this tab's own listeners would be wrong (the task's `metadata`/`data` can carry
|
|
235
|
-
* PII), inspect `detail` here and check whatever your app considers "still valid" — the same
|
|
236
|
-
* way `claimResultOnce` lets you gate the leader's own outgoing dispatch. This is the
|
|
237
|
-
* receiving-side half of that same concern; without it, there was previously no way to
|
|
238
|
-
* intercept an inbound relayed result at all.
|
|
239
|
-
*
|
|
240
|
-
* Evaluated synchronously with no `await` before the dispatch it gates (unlike
|
|
241
|
-
* `claimResultOnce`, there's no cross-tab claim to arbitrate on this side — only this tab
|
|
242
|
-
* decides whether to act on what it received, so there's no lock-arbitration window for
|
|
243
|
-
* your condition to go stale in between). If your own check is inherently async (e.g. reads
|
|
244
|
-
* from IndexedDB), resolve it eagerly elsewhere and read a synchronous flag here rather than
|
|
245
|
-
* awaiting inline.
|
|
246
|
-
*/
|
|
247
|
-
acceptRelayedResult?: (detail: PendingTaskResultEventDetail<TType>) => boolean;
|
|
248
|
-
}
|
|
249
|
-
/**
|
|
250
|
-
* Framework-agnostic polling engine: scans the store's tasks on an interval, calls the
|
|
251
|
-
* matching handler's `check()`, and resolves each task to `pending` (re-check later),
|
|
252
|
-
* `success`/`failure` (dispatched via `onResult`, then removed), silently-or-not `error`
|
|
253
|
-
* (removed; dispatched unless `silentOnFailure`) when `check()` itself kept failing, or
|
|
254
|
-
* silently expired (removed, never dispatched — unless the handler opts into
|
|
255
|
-
* `finalCheckOnExpiry` for one last check).
|
|
256
|
-
*
|
|
257
|
-
* When multiple tabs share a store, `crossTabPollLeaderElection` (on by default) ensures only
|
|
258
|
-
* one of them actually polls at a time — see that option and `resultRelayKey` for how the
|
|
259
|
-
* others still learn about results without polling themselves.
|
|
260
|
-
*
|
|
261
|
-
* Framework bindings (see `./react`) are thin wrappers that call `start()`/`stop()` at the
|
|
262
|
-
* right lifecycle moments and expose `forceCheckAll()` for e.g. tab-focus recovery.
|
|
263
|
-
*
|
|
264
|
-
* Note: `stop()` prevents any *new* tick from starting, but a tick already awaiting
|
|
265
|
-
* `handler.check()` when `stop()` is called will still run to completion (there is no
|
|
266
|
-
* `AbortSignal` plumbed into the handler contract). Design handlers to be safe to finish
|
|
267
|
-
* even if the caller has logically "stopped" — e.g. don't assume side effects are undone.
|
|
268
|
-
*/
|
|
269
|
-
declare class PendingTaskPoller<TType extends string = string> {
|
|
270
|
-
private readonly options;
|
|
271
|
-
/** Stable for this instance's whole lifetime — e.g. one `PendingTaskPoller` construction per
|
|
272
|
-
* browser tab (that's how the React binding uses it). Regenerating this per claim would make
|
|
273
|
-
* a tab unable to recognize its own still-valid lease as "mine" on the next renewal. */
|
|
274
|
-
private readonly ownerId;
|
|
275
|
-
private readonly pollLease;
|
|
276
|
-
private intervalId;
|
|
277
|
-
private storageListener;
|
|
278
|
-
private isChecking;
|
|
279
|
-
private pendingForce;
|
|
280
|
-
private stopped;
|
|
281
|
-
private latestTasksCache;
|
|
282
|
-
/** Task ids that already got their one `finalCheckOnExpiry` attempt, so a repeatedly-failing
|
|
283
|
-
* final check doesn't get retried every tick. Reset on process restart — worst case that
|
|
284
|
-
* costs one extra check, never an infinite retry loop.
|
|
285
|
-
*
|
|
286
|
-
* Every id added here (only when a task is expired, right before its final `check()`) is
|
|
287
|
-
* removed again before the *same* tick's iteration moves past that task — either by
|
|
288
|
-
* `finalize()`'s first line, or by one of the leadership-loss/`onCheckError`-intercept
|
|
289
|
-
* branches that bail out without ever reaching `finalize()`. Nothing here is meant to
|
|
290
|
-
* survive past the tick that added it. */
|
|
291
|
-
private readonly finalCheckAttempted;
|
|
292
|
-
constructor(options: PendingTaskPollerOptions<TType>);
|
|
293
|
-
start(): void;
|
|
294
|
-
stop(): void;
|
|
295
|
-
/** Re-check every tracked task right now, bypassing each task's poll interval (e.g. on tab focus). */
|
|
296
|
-
forceCheckAll(): void;
|
|
297
|
-
private claimLeadership;
|
|
298
|
-
/**
|
|
299
|
-
* Re-confirms that poll leadership is still this tab's — and still the *same continuous
|
|
300
|
-
* tenure* as when `fence` was captured, not just "is nobody else currently holding it" (a
|
|
301
|
-
* no-op returning `fence` unchanged when `crossTabPollLeaderElection` is off). Clears `task`'s
|
|
302
|
-
* `finalCheckAttempted` bookkeeping and returns `false` if not — the caller should stop
|
|
303
|
-
* treating this tick's remaining due tasks as network-eligible (though it may still process
|
|
304
|
-
* ones that need no leadership) rather than act on a possibly-stale outcome.
|
|
305
|
-
*
|
|
306
|
-
* A fence mismatch (rather than just an owner-id mismatch) is needed to catch leadership
|
|
307
|
-
* having churned through another tab and back to this one while a slow `handler.check()` was
|
|
308
|
-
* in flight: this tab's lease can expire mid-check, another tab claims it and fully resolves
|
|
309
|
-
* the same task, and that tab's own lease can *also* expire before this tab's stale response
|
|
310
|
-
* comes back — at which point this tab's next claim legitimately succeeds under its own
|
|
311
|
-
* stable owner id (nothing currently holds the lease), even though leadership genuinely
|
|
312
|
-
* changed hands in between. See `PollLeaseClaimResult`.
|
|
313
|
-
*/
|
|
314
|
-
private reconfirmLeadership;
|
|
315
|
-
private releaseLeadership;
|
|
316
|
-
/** Fires `runTick`, but instead of leaving its promise `void`-called (which would turn an
|
|
317
|
-
* exception thrown by a consumer callback — `onResult`, `onCheckError`, or a `dispatchEvent`
|
|
318
|
-
* listener — into a silent unhandled rejection), re-throws it as an uncaught exception on a
|
|
319
|
-
* fresh microtask. `runTick`'s own `finally` has already flushed the batch and reset
|
|
320
|
-
* `isChecking` by the time this ever runs, so a broken consumer callback can't take the
|
|
321
|
-
* poller down — it just becomes visible the way any other uncaught error in the host
|
|
322
|
-
* environment would be, instead of vanishing. */
|
|
323
|
-
private runTickSafely;
|
|
324
|
-
/** Reads the freshest snapshot of `task` from the store, in case another tab wrote to it
|
|
325
|
-
* while this tab's `handler.check()` was in flight — narrows, but doesn't eliminate, the
|
|
326
|
-
* window where a concurrent cross-tab write to the same task could be clobbered.
|
|
327
|
-
*
|
|
328
|
-
* Indexes `store.getState().tasks` into a Map keyed by id rather than doing a linear find
|
|
329
|
-
* each call — this is called once per pending/failing task per tick, so a plain find would
|
|
330
|
-
* make a tick O(n²). The cache keys off the `tasks` array reference, which zustand only
|
|
331
|
-
* replaces on an actual write, so it's rebuilt only when the store has genuinely changed. */
|
|
332
|
-
private getLatestTask;
|
|
333
|
-
/** Applies a whole tick's worth of per-task updates/removals (`patch: null` means "remove")
|
|
334
|
-
* in a single read-modify-write, instead of one persisted-storage round trip per task.
|
|
335
|
-
* Reads the freshest persisted list right before writing (same "never resurrect a task
|
|
336
|
-
* another tab already removed" guarantee `PendingTaskStore`'s own mutators give) — unless
|
|
337
|
-
* the store's `hasUnpersistedWrites` is set (a write on *any* path for this store, including
|
|
338
|
-
* this store's own direct mutators, failed and hasn't yet been followed by a success), in
|
|
339
|
-
* which case persisted storage is stale relative to this tab's memory, so this flush builds
|
|
340
|
-
* on `store.getState().tasks` instead. Writes through `writeTasks`, which swallows a
|
|
341
|
-
* throwing write and updates that same shared flag — see the comment in `store.ts`. */
|
|
342
|
-
private flushBatch;
|
|
343
|
-
private finalize;
|
|
344
|
-
/**
|
|
345
|
-
* Handles a result relayed from another tab's leader — mirrors finalize()'s own
|
|
346
|
-
* `claimResultOnce` gate and dispatch, so a `claimResultOnce` composed for "one notification
|
|
347
|
-
* system-wide" (see its doc comment's "keep both if you want both properties") applies
|
|
348
|
-
* uniformly whether this tab detected the result itself or only learned about it via the
|
|
349
|
-
* relay, not just to the narrower direct-detection race `claimResultOnce` guarded before this
|
|
350
|
-
* relay existed.
|
|
351
|
-
*
|
|
352
|
-
* Fired-and-forgotten (`void`-called) from the "storage" listener rather than awaited, so it
|
|
353
|
-
* has no `this.stopped` check of its own: `stop()` removes the listener (no *new* relayed
|
|
354
|
-
* result starts one of these after that), but one already in flight when `stop()` is called
|
|
355
|
-
* (e.g. awaiting a slow `claimResultOnce`) still runs to completion — the same tolerance
|
|
356
|
-
* `runTick`'s own doc comment describes for an in-flight tick.
|
|
357
|
-
*/
|
|
358
|
-
private dispatchRelayedResult;
|
|
359
|
-
private runTick;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
export { type CreatePendingTaskStoreOptions as C, DEFAULT_MAX_FAILURE_COUNT as D, type PendingTaskStore as P, type PendingTaskRegistry as a, type PendingTask as b, type PendingTaskResultEventDetail as c, DEFAULT_POLL_INTERVAL_MS as d, DEFAULT_POLL_LEASE_TTL_MULTIPLIER as e, DEFAULT_POLL_TICK_MS as f, DEFAULT_RESULT_EVENT as g, DEFAULT_STORAGE_KEY as h, DEFAULT_TTL_MS as i, type PendingTaskCheckResult as j, type PendingTaskHandler as k, type PendingTaskMetadata as l, PendingTaskPoller as m, type PendingTaskPollerOptions as n, type PendingTaskResultStatus as o, type PendingTaskStatus as p, type PendingTaskStoreState as q, createPendingTaskStore as r, isPendingTaskShape as s, parseTasksFromStorageValue as t };
|