@abloatai/ablo 0.12.0 → 0.14.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/AGENTS.md +2 -2
- package/CHANGELOG.md +29 -0
- package/README.md +3 -3
- package/dist/BaseSyncedStore.js +39 -32
- package/dist/batching/index.d.ts +57 -0
- package/dist/batching/index.js +150 -0
- package/dist/cli.cjs +158 -40
- package/dist/client/Ablo.d.ts +16 -25
- package/dist/client/Ablo.js +1 -1
- package/dist/client/auth.js +11 -0
- package/dist/client/createModelProxy.d.ts +33 -8
- package/dist/client/createModelProxy.js +4 -4
- package/dist/errorCodes.d.ts +3 -1
- package/dist/errorCodes.js +10 -1
- package/dist/schema/index.d.ts +2 -2
- package/dist/schema/index.js +2 -2
- package/dist/schema/model.d.ts +38 -84
- package/dist/schema/model.js +12 -12
- package/dist/schema/roles.d.ts +49 -0
- package/dist/schema/roles.js +21 -0
- package/dist/schema/schema.d.ts +1 -1
- package/dist/schema/schema.js +1 -1
- package/dist/schema/serialize.d.ts +4 -2
- package/dist/schema/serialize.js +4 -2
- package/dist/schema/sugar.d.ts +7 -28
- package/dist/schema/sugar.js +2 -7
- package/dist/schema/sync-delta-row.d.ts +2 -0
- package/dist/schema/sync-delta-row.js +2 -1
- package/dist/schema/tenancy.d.ts +67 -28
- package/dist/schema/tenancy.js +93 -23
- package/dist/server/commit.d.ts +8 -3
- package/docs/api.md +7 -6
- package/docs/cli.md +43 -4
- package/docs/client-behavior.md +2 -2
- package/docs/coordination.md +12 -12
- package/docs/examples/agent-human.md +6 -6
- package/docs/examples/ai-sdk-tool.md +1 -1
- package/docs/examples/existing-python-backend.md +0 -2
- package/docs/examples/nextjs.md +2 -2
- package/docs/examples/scoped-agent.md +3 -3
- package/docs/examples/server-agent.md +4 -4
- package/docs/identity.md +27 -20
- package/docs/index.md +0 -1
- package/docs/integration-guide.md +12 -9
- package/docs/interaction-model.md +1 -1
- package/docs/mcp.md +17 -5
- package/docs/quickstart.md +3 -3
- package/docs/react.md +69 -0
- package/llms.txt +2 -3
- package/package.json +8 -2
- package/docs/mcp/claude-code.md +0 -35
- package/docs/mcp/cursor.md +0 -35
- package/docs/mcp/windsurf.md +0 -33
- package/docs/roadmap.md +0 -55
- package/docs/the-loop.md +0 -21
- package/llms-full.txt +0 -396
package/AGENTS.md
CHANGED
|
@@ -31,7 +31,7 @@ Every model verb takes ONE options object. The common loop:
|
|
|
31
31
|
|
|
32
32
|
1. **Read** the row — `await ablo.<model>.retrieve({ id })` (async; from the server) or `await ablo.<model>.list({ where })` for many. In React render, read synchronously with `useAblo((a) => a.<model>.get(id))`.
|
|
33
33
|
2. **See who's active** (optional) — `ablo.<model>.claim.state({ id })` (synchronous; never blocks).
|
|
34
|
-
3. **Claim** the row before changing it — `await using claim = await ablo.<model>.claim({ id,
|
|
34
|
+
3. **Claim** the row before changing it — `await using claim = await ablo.<model>.claim({ id, reason?, ttl? })`. If someone else holds it, this waits for them, then gives you the fresh row on `claim.data`. The claim auto-releases when it goes out of scope (`await using`).
|
|
35
35
|
4. **Write** — `await ablo.<model>.update({ id: claim.data.id, data })`. Because you hold the claim, the write is rejected if the row changed underneath you.
|
|
36
36
|
|
|
37
37
|
Keep coding assistants on this schema-backed path.
|
|
@@ -59,7 +59,7 @@ if (!report) throw new Error('Report not found');
|
|
|
59
59
|
// row before resolving. Auto-released at the end of this scope (`await using`).
|
|
60
60
|
await using claim = await ablo.weatherReports.claim({
|
|
61
61
|
id: 'report_stockholm',
|
|
62
|
-
|
|
62
|
+
reason: 'forecasting',
|
|
63
63
|
ttl: '2m',
|
|
64
64
|
});
|
|
65
65
|
const claimed = claim.data;
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.14.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Claim API consistency + coordination docs
|
|
8
|
+
- **React:** document `useWatch` (scoped presence + read-interest, with `claim`/`hydrate`/`paused` options) and `usePeers` (read-only presence) — previously exported but undocumented.
|
|
9
|
+
- **HTTP claim surface:** `HttpClaimApi` is now a mechanically derived async projection of the reactive `ClaimApi` (`AwaitedClaimMethod`), so the two transports can never drift. No behavior change — the only difference remains the `Promise` wrapper that statelessness forces on `state`/`queue`/`reorder`.
|
|
10
|
+
- **Naming:** unified the claim read verb to `state` across every layer (the internal `ModelCollaboration.observe` is now `state`, matching the public `ablo.<model>.claim.state({ id })`).
|
|
11
|
+
- **Docs:** corrected the `Claim` object reference — the field is `reason` (serialized on the wire as `action`), and `createdAt`/`expiresAt` are `number` (epoch-ms), not strings; corrected the claim options to `reason` and `queue`.
|
|
12
|
+
|
|
13
|
+
## 0.13.0
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- Schema authoring: split model routing into two orthogonal axes — `policy` (row access) and `groups` (sync-group routing).
|
|
18
|
+
|
|
19
|
+
**Breaking (schema authoring).** The flat, collision-prone model options are replaced by two namespaced ones:
|
|
20
|
+
- **`policy`** — row-access / tenant isolation (named after Postgres/Supabase RLS policies: the rule that scopes which rows a tenant may read). A discriminated union on `by` replaces the old `orgScoped` / `scopedVia` / `orgColumn` trio:
|
|
21
|
+
- `{ by: 'column' }` — row-local tenancy column (the default when omitted; column name still overridable).
|
|
22
|
+
- `{ by: 'parent', fk, parent }` — inherit tenancy through a foreign key when the table has no tenancy column of its own (e.g. `slide_layers` → `slides`).
|
|
23
|
+
- Type `TenancyInput` is renamed `PolicyInput`; `policyInputSchema` / `resolvePolicy` are now exported.
|
|
24
|
+
- **`groups: { root, grants, roles }`** — which delta channels a row fans into (orthogonal to `policy`, which governs read access). One namespaced object replaces the old flat `scope` / `grants` / `entityRoles`:
|
|
25
|
+
- `root` (was `scope`) — mark a model a scope root; its records form the group `<kind>:<id>`. Renamed so it no longer collides with the old `scopedVia` tenancy sugar or the inner `grants.scope` relation name.
|
|
26
|
+
- `grants` — a membership edge granting an identity access to a scope root.
|
|
27
|
+
- `roles` (was `entityRoles`) — explicit non-relational record→group roles; accepts one role or an array.
|
|
28
|
+
- `groupsInputSchema` / `GroupsInput` are now exported.
|
|
29
|
+
|
|
30
|
+
**CLI.** `config.json` now stores per-project profile key pairs (`profiles: Record<string, ProfileKeys>`) instead of a single top-level pair; older flat layouts are folded into the active profile automatically on read, so existing logins keep working. `login` / `projects` updated to the profile model.
|
|
31
|
+
|
|
3
32
|
## 0.12.0
|
|
4
33
|
|
|
5
34
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -274,7 +274,7 @@ ablo.weatherReports.claim.state({ id: 'report_stockholm' });
|
|
|
274
274
|
ablo.weatherReports.claim.queue({ id: 'report_stockholm' });
|
|
275
275
|
|
|
276
276
|
{
|
|
277
|
-
await using claim = await ablo.weatherReports.claim({ id,
|
|
277
|
+
await using claim = await ablo.weatherReports.claim({ id, queue: false });
|
|
278
278
|
/* do the held work */
|
|
279
279
|
}
|
|
280
280
|
|
|
@@ -285,11 +285,11 @@ ablo.weatherReports.claim.queue({ id: 'report_stockholm' });
|
|
|
285
285
|
```
|
|
286
286
|
|
|
287
287
|
`claim.state` returns the holder (or `null`); `claim.queue` returns the line waiting
|
|
288
|
-
behind it. `
|
|
288
|
+
behind it. `queue: false` skips rather than waiting when the row is held;
|
|
289
289
|
`maxQueueDepth: 2` bails when two or more are already ahead.
|
|
290
290
|
|
|
291
291
|
Default reads keep working while a row is claimed. Server reads that need claimed
|
|
292
|
-
semantics can opt in with `ifClaimed: 'return' | '
|
|
292
|
+
semantics can opt in with `ifClaimed: 'return' | 'fail'`.
|
|
293
293
|
|
|
294
294
|
Even an unclaimed write can't land on stale reasoning — the commit is guarded:
|
|
295
295
|
|
package/dist/BaseSyncedStore.js
CHANGED
|
@@ -781,47 +781,54 @@ export class BaseSyncedStore {
|
|
|
781
781
|
startCredentialLifecycle(getToken) {
|
|
782
782
|
this.stopCredentialLifecycle();
|
|
783
783
|
this.setCredentialRefresher(getToken);
|
|
784
|
-
//
|
|
785
|
-
//
|
|
786
|
-
//
|
|
784
|
+
// Re-mint through the SAME single-flight path the FSM's reactive probe uses
|
|
785
|
+
// (`performCredentialRefresh`) rather than calling `getToken()` directly. Two
|
|
786
|
+
// wins over the old direct call:
|
|
787
|
+
// - SINGLE-FLIGHT: a wake nudge, an in-flight probe, and this proactive
|
|
788
|
+
// roll share one in-flight promise — no double-mint thrash.
|
|
789
|
+
// - The tri-state is HONOURED. The old code did `if (token) {…}` and
|
|
790
|
+
// dropped a `null` on the floor — a zombie session that re-minted on
|
|
791
|
+
// every tab focus and logged "signing out" forever without ever signing
|
|
792
|
+
// out. `session_error` now drives the FSM to actually expire.
|
|
787
793
|
const refresh = async () => {
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
// connection to re-probe with the fresh key. Same two steps the
|
|
794
|
-
// engine's `setAuthToken` wrapper performs.
|
|
795
|
-
this.auth?.setAuthToken(token);
|
|
796
|
-
this.nudgeReconnect();
|
|
797
|
-
}
|
|
794
|
+
const outcome = await this.performCredentialRefresh();
|
|
795
|
+
if (outcome === 'refreshed') {
|
|
796
|
+
// Fresh key already pushed into the credential source by
|
|
797
|
+
// `performCredentialRefresh`; nudge a parked connection to re-probe.
|
|
798
|
+
this.nudgeReconnect();
|
|
798
799
|
}
|
|
799
|
-
|
|
800
|
-
//
|
|
800
|
+
else if (outcome === 'session_error') {
|
|
801
|
+
// The long-lived login is gone (mint answered 401/403). Surface it —
|
|
802
|
+
// the proactive path's job is to report this, not hide it. A no-op in
|
|
803
|
+
// FSM states that don't accept the event (the probe converges on
|
|
804
|
+
// sign-out there anyway); `session_expired`'s onEnter owns the log.
|
|
805
|
+
this.connectionManager?.send({ type: 'BOOTSTRAP_FAILED_SESSION' });
|
|
801
806
|
}
|
|
807
|
+
// 'network_error' → transient (offline / mint hiccup); the next timer tick
|
|
808
|
+
// or the FSM's own probe retries. Never sign out for it.
|
|
802
809
|
};
|
|
803
810
|
// Comfortably inside the 15m `ek_` TTL; a missed (background-throttled) tick
|
|
804
|
-
// is recovered by the next, or by the reactive probe.
|
|
811
|
+
// is recovered by the next, or by the reactive probe. The timer is the sole
|
|
812
|
+
// proactive PRE-ROLL — it keeps the key warm ahead of expiry even while the
|
|
813
|
+
// socket sits healthy-`connected` (a state the FSM never probes unprompted).
|
|
805
814
|
const REFRESH_INTERVAL_MS = 10 * 60 * 1000;
|
|
806
815
|
const timer = setInterval(() => void refresh(), REFRESH_INTERVAL_MS);
|
|
807
816
|
const teardowns = [() => clearInterval(timer)];
|
|
808
817
|
if (typeof window !== 'undefined') {
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
//
|
|
812
|
-
//
|
|
813
|
-
//
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
document.addEventListener('visibilitychange', onVisible);
|
|
824
|
-
teardowns.push(() => document.removeEventListener('visibilitychange', onVisible));
|
|
818
|
+
// OS-wake (desktop only): the Electron shell bridges `powerMonitor`
|
|
819
|
+
// 'resume' to this DOM event. This is the ONE event-trigger the lifecycle
|
|
820
|
+
// still owns, because `visibilitychange` does NOT fire on wake-from-sleep
|
|
821
|
+
// and — unlike `online`/`visibilitychange` — the ConnectionManager's own
|
|
822
|
+
// browser listeners (`setupBrowserListeners`) don't cover wake.
|
|
823
|
+
//
|
|
824
|
+
// The `online` and `visibilitychange` listeners that used to live here
|
|
825
|
+
// were REMOVED: the FSM already re-probes on NETWORK_ONLINE / TAB_VISIBLE
|
|
826
|
+
// through this exact credential path, so registering them here too only
|
|
827
|
+
// fired a second, null-swallowing mint per focus — the "session-key
|
|
828
|
+
// POSTed on every tab focus" spam in the console.
|
|
829
|
+
const onWake = () => void refresh();
|
|
830
|
+
window.addEventListener('ablo:wake', onWake);
|
|
831
|
+
teardowns.push(() => window.removeEventListener('ablo:wake', onWake));
|
|
825
832
|
}
|
|
826
833
|
this.credentialLifecycleTeardown = () => {
|
|
827
834
|
for (const t of teardowns)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@abloatai/ablo/batching` — a dependency-free batch-coalescing primitive.
|
|
3
|
+
*
|
|
4
|
+
* Accumulate items issued close together (the canonical case: a synchronous
|
|
5
|
+
* burst, e.g. `Promise.all([ a(), b(), c() ])` in one event-loop tick) and
|
|
6
|
+
* dispatch them as ONE atomic batch instead of one call each. This is the
|
|
7
|
+
* scheduling essence of Ablo's `TransactionQueue` (and Linear's sync engine) —
|
|
8
|
+
* microtask same-tick staging, size/cost/delay flush triggers, and in-flight
|
|
9
|
+
* backpressure — distilled to a pure state machine with NO dependency on
|
|
10
|
+
* models, MobX, IndexedDB, or the wire. Consumers inject the actual dispatch.
|
|
11
|
+
*
|
|
12
|
+
* Guarantees:
|
|
13
|
+
* - a batch is ONE `dispatchBatch(items)` call → **atomic** (all-or-nothing).
|
|
14
|
+
* - on dispatch failure, **every** enqueued promise in that batch rejects
|
|
15
|
+
* with the same error.
|
|
16
|
+
* - items dispatch in enqueue order (optionally reordered by `compare` just
|
|
17
|
+
* before a batch is cut); batches run FIFO under a `maxInFlight` cap.
|
|
18
|
+
*
|
|
19
|
+
* The slides-sdk wraps this to coalesce `commits.create` calls; the stateful
|
|
20
|
+
* `TransactionQueue` MAY adopt it later (it would supply `compare` for FK
|
|
21
|
+
* ordering and keep its merge/confirm/retry logic in its own hooks).
|
|
22
|
+
*/
|
|
23
|
+
export interface BatchSchedulerOptions<T> {
|
|
24
|
+
/** Master switch. When false, every `enqueue` dispatches solo immediately. Default true. */
|
|
25
|
+
readonly enabled?: boolean;
|
|
26
|
+
/** Coalescing window in ms. `0` (default) → flush on the next microtask (zero added latency). */
|
|
27
|
+
readonly windowMs?: number;
|
|
28
|
+
/** Max items per batch before a forced flush. Default 256. */
|
|
29
|
+
readonly maxBatchSize?: number;
|
|
30
|
+
/** Max accumulated `costOf` per batch before a forced flush. Default `Infinity` (disabled). */
|
|
31
|
+
readonly maxBatchCost?: number;
|
|
32
|
+
/** Per-item cost used by `maxBatchCost` (e.g. serialized bytes). Default `() => 0`. */
|
|
33
|
+
readonly costOf?: (item: T) => number;
|
|
34
|
+
/** Max dispatches in flight at once (backpressure). Default 1 → strictly ordered. */
|
|
35
|
+
readonly maxInFlight?: number;
|
|
36
|
+
}
|
|
37
|
+
export interface BatchSchedulerHooks<T, R> {
|
|
38
|
+
/** The single dispatch for one batch. One call → atomic at this layer. */
|
|
39
|
+
dispatchBatch(items: T[]): Promise<R>;
|
|
40
|
+
/**
|
|
41
|
+
* Optional ordering applied to the staged items immediately before a batch
|
|
42
|
+
* is cut (e.g. FK-priority). Omit for FIFO. Does not affect which items share
|
|
43
|
+
* a batch — only their order within the dispatched array.
|
|
44
|
+
*/
|
|
45
|
+
compare?(a: T, b: T): number;
|
|
46
|
+
}
|
|
47
|
+
export interface BatchScheduler<T, R> {
|
|
48
|
+
/** Stage one item; resolves with its batch's dispatch result, or rejects with the batch error. */
|
|
49
|
+
enqueue(item: T): Promise<R>;
|
|
50
|
+
/** Stage an item that must dispatch in its OWN batch (e.g. it carries an explicit idempotency key). */
|
|
51
|
+
enqueueSolo(item: T): Promise<R>;
|
|
52
|
+
/** Force-flush the pending batch and resolve once everything in flight has settled. */
|
|
53
|
+
flush(): Promise<void>;
|
|
54
|
+
/** Stop scheduling and clear timers. Pending/in-flight promises still settle. */
|
|
55
|
+
dispose(): void;
|
|
56
|
+
}
|
|
57
|
+
export declare function createBatchScheduler<T, R>(hooks: BatchSchedulerHooks<T, R>, options?: BatchSchedulerOptions<T>): BatchScheduler<T, R>;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@abloatai/ablo/batching` — a dependency-free batch-coalescing primitive.
|
|
3
|
+
*
|
|
4
|
+
* Accumulate items issued close together (the canonical case: a synchronous
|
|
5
|
+
* burst, e.g. `Promise.all([ a(), b(), c() ])` in one event-loop tick) and
|
|
6
|
+
* dispatch them as ONE atomic batch instead of one call each. This is the
|
|
7
|
+
* scheduling essence of Ablo's `TransactionQueue` (and Linear's sync engine) —
|
|
8
|
+
* microtask same-tick staging, size/cost/delay flush triggers, and in-flight
|
|
9
|
+
* backpressure — distilled to a pure state machine with NO dependency on
|
|
10
|
+
* models, MobX, IndexedDB, or the wire. Consumers inject the actual dispatch.
|
|
11
|
+
*
|
|
12
|
+
* Guarantees:
|
|
13
|
+
* - a batch is ONE `dispatchBatch(items)` call → **atomic** (all-or-nothing).
|
|
14
|
+
* - on dispatch failure, **every** enqueued promise in that batch rejects
|
|
15
|
+
* with the same error.
|
|
16
|
+
* - items dispatch in enqueue order (optionally reordered by `compare` just
|
|
17
|
+
* before a batch is cut); batches run FIFO under a `maxInFlight` cap.
|
|
18
|
+
*
|
|
19
|
+
* The slides-sdk wraps this to coalesce `commits.create` calls; the stateful
|
|
20
|
+
* `TransactionQueue` MAY adopt it later (it would supply `compare` for FK
|
|
21
|
+
* ordering and keep its merge/confirm/retry logic in its own hooks).
|
|
22
|
+
*/
|
|
23
|
+
export function createBatchScheduler(hooks, options) {
|
|
24
|
+
const enabled = options?.enabled ?? true;
|
|
25
|
+
const windowMs = options?.windowMs ?? 0;
|
|
26
|
+
const maxBatchSize = options?.maxBatchSize ?? 256;
|
|
27
|
+
const maxBatchCost = options?.maxBatchCost ?? Infinity;
|
|
28
|
+
const costOf = options?.costOf ?? (() => 0);
|
|
29
|
+
const maxInFlight = options?.maxInFlight ?? 1;
|
|
30
|
+
let pending = null;
|
|
31
|
+
let timer = null;
|
|
32
|
+
let microtaskScheduled = false;
|
|
33
|
+
const ready = [];
|
|
34
|
+
let inFlight = 0;
|
|
35
|
+
let idleWaiters = [];
|
|
36
|
+
let disposed = false;
|
|
37
|
+
function scheduleFlush() {
|
|
38
|
+
if (microtaskScheduled || timer)
|
|
39
|
+
return;
|
|
40
|
+
if (windowMs > 0) {
|
|
41
|
+
timer = setTimeout(() => {
|
|
42
|
+
timer = null;
|
|
43
|
+
flushPending();
|
|
44
|
+
}, windowMs);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
microtaskScheduled = true;
|
|
48
|
+
queueMicrotask(() => {
|
|
49
|
+
microtaskScheduled = false;
|
|
50
|
+
flushPending();
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function flushPending() {
|
|
55
|
+
if (timer) {
|
|
56
|
+
clearTimeout(timer);
|
|
57
|
+
timer = null;
|
|
58
|
+
}
|
|
59
|
+
microtaskScheduled = false;
|
|
60
|
+
if (!pending)
|
|
61
|
+
return;
|
|
62
|
+
if (hooks.compare)
|
|
63
|
+
pending.items.sort(hooks.compare);
|
|
64
|
+
ready.push(pending);
|
|
65
|
+
pending = null;
|
|
66
|
+
pump();
|
|
67
|
+
}
|
|
68
|
+
function pump() {
|
|
69
|
+
while (inFlight < maxInFlight && ready.length > 0) {
|
|
70
|
+
const batch = ready.shift();
|
|
71
|
+
if (!batch)
|
|
72
|
+
break;
|
|
73
|
+
inFlight++;
|
|
74
|
+
let dispatched;
|
|
75
|
+
try {
|
|
76
|
+
dispatched = hooks.dispatchBatch(batch.items);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
dispatched = Promise.reject(error);
|
|
80
|
+
}
|
|
81
|
+
dispatched
|
|
82
|
+
.then((result) => {
|
|
83
|
+
for (const d of batch.deferreds)
|
|
84
|
+
d.resolve(result);
|
|
85
|
+
}, (error) => {
|
|
86
|
+
for (const d of batch.deferreds)
|
|
87
|
+
d.reject(error);
|
|
88
|
+
})
|
|
89
|
+
.finally(() => {
|
|
90
|
+
inFlight--;
|
|
91
|
+
pump();
|
|
92
|
+
notifyIdleIfDrained();
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function notifyIdleIfDrained() {
|
|
97
|
+
if (inFlight === 0 && ready.length === 0 && idleWaiters.length > 0) {
|
|
98
|
+
const waiters = idleWaiters;
|
|
99
|
+
idleWaiters = [];
|
|
100
|
+
for (const w of waiters)
|
|
101
|
+
w();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function enqueueSolo(item) {
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
ready.push({ items: [item], deferreds: [{ resolve, reject }], cost: costOf(item) });
|
|
107
|
+
pump();
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
function enqueue(item) {
|
|
111
|
+
if (disposed)
|
|
112
|
+
return Promise.reject(new Error('batch scheduler disposed'));
|
|
113
|
+
if (!enabled)
|
|
114
|
+
return enqueueSolo(item);
|
|
115
|
+
const cost = costOf(item);
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
const deferred = { resolve, reject };
|
|
118
|
+
// Rollover: if appending would blow a cap, flush the current batch first.
|
|
119
|
+
if (pending && (pending.items.length + 1 > maxBatchSize || pending.cost + cost > maxBatchCost)) {
|
|
120
|
+
flushPending();
|
|
121
|
+
}
|
|
122
|
+
if (!pending)
|
|
123
|
+
pending = { items: [], deferreds: [], cost: 0 };
|
|
124
|
+
pending.items.push(item);
|
|
125
|
+
pending.deferreds.push(deferred);
|
|
126
|
+
pending.cost += cost;
|
|
127
|
+
if (pending.items.length >= maxBatchSize || pending.cost >= maxBatchCost) {
|
|
128
|
+
flushPending();
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
scheduleFlush();
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
async function flush() {
|
|
136
|
+
flushPending();
|
|
137
|
+
while (ready.length > 0 || inFlight > 0) {
|
|
138
|
+
await new Promise((resolve) => idleWaiters.push(resolve));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function dispose() {
|
|
142
|
+
disposed = true;
|
|
143
|
+
if (timer) {
|
|
144
|
+
clearTimeout(timer);
|
|
145
|
+
timer = null;
|
|
146
|
+
}
|
|
147
|
+
microtaskScheduled = false;
|
|
148
|
+
}
|
|
149
|
+
return { enqueue, enqueueSolo, flush, dispose };
|
|
150
|
+
}
|