@abloatai/ablo 0.3.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/CHANGELOG.md +208 -0
- package/LICENSE +201 -0
- package/NOTICE +12 -0
- package/README.md +230 -0
- package/dist/BaseSyncedStore.d.ts +709 -0
- package/dist/BaseSyncedStore.js +1843 -0
- package/dist/Database.d.ts +344 -0
- package/dist/Database.js +1259 -0
- package/dist/LazyReferenceCollection.d.ts +181 -0
- package/dist/LazyReferenceCollection.js +460 -0
- package/dist/Model.d.ts +339 -0
- package/dist/Model.js +715 -0
- package/dist/ModelRegistry.d.ts +200 -0
- package/dist/ModelRegistry.js +535 -0
- package/dist/NetworkMonitor.d.ts +27 -0
- package/dist/NetworkMonitor.js +73 -0
- package/dist/ObjectPool.d.ts +202 -0
- package/dist/ObjectPool.js +1106 -0
- package/dist/SyncClient.d.ts +489 -0
- package/dist/SyncClient.js +1555 -0
- package/dist/SyncEngineContext.d.ts +46 -0
- package/dist/SyncEngineContext.js +74 -0
- package/dist/adapters/alwaysOnline.d.ts +16 -0
- package/dist/adapters/alwaysOnline.js +19 -0
- package/dist/adapters/inMemoryStorage.d.ts +30 -0
- package/dist/adapters/inMemoryStorage.js +94 -0
- package/dist/agent/Agent.d.ts +358 -0
- package/dist/agent/Agent.js +500 -0
- package/dist/agent/index.d.ts +115 -0
- package/dist/agent/index.js +128 -0
- package/dist/agent/session.d.ts +90 -0
- package/dist/agent/session.js +156 -0
- package/dist/agent/types.d.ts +73 -0
- package/dist/agent/types.js +10 -0
- package/dist/ai-sdk/coordination-context.d.ts +51 -0
- package/dist/ai-sdk/coordination-context.js +107 -0
- package/dist/ai-sdk/index.d.ts +68 -0
- package/dist/ai-sdk/index.js +68 -0
- package/dist/ai-sdk/intent-broadcast.d.ts +77 -0
- package/dist/ai-sdk/intent-broadcast.js +72 -0
- package/dist/ai-sdk/wrap.d.ts +67 -0
- package/dist/ai-sdk/wrap.js +45 -0
- package/dist/api/index.d.ts +10 -0
- package/dist/api/index.js +9 -0
- package/dist/auth/index.d.ts +137 -0
- package/dist/auth/index.js +246 -0
- package/dist/client/Ablo.d.ts +835 -0
- package/dist/client/Ablo.js +1440 -0
- package/dist/client/ApiClient.d.ts +200 -0
- package/dist/client/ApiClient.js +659 -0
- package/dist/client/auth.d.ts +79 -0
- package/dist/client/auth.js +81 -0
- package/dist/client/createInternalComponents.d.ts +44 -0
- package/dist/client/createInternalComponents.js +88 -0
- package/dist/client/createModelProxy.d.ts +152 -0
- package/dist/client/createModelProxy.js +199 -0
- package/dist/client/identity.d.ts +63 -0
- package/dist/client/identity.js +156 -0
- package/dist/client/index.d.ts +36 -0
- package/dist/client/index.js +33 -0
- package/dist/client/persistence.d.ts +7 -0
- package/dist/client/persistence.js +11 -0
- package/dist/client/validateAbloOptions.d.ts +42 -0
- package/dist/client/validateAbloOptions.js +43 -0
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.js +12 -0
- package/dist/context.d.ts +27 -0
- package/dist/context.js +58 -0
- package/dist/core/DatabaseManager.d.ts +108 -0
- package/dist/core/DatabaseManager.js +361 -0
- package/dist/core/QueryProcessor.d.ts +77 -0
- package/dist/core/QueryProcessor.js +262 -0
- package/dist/core/QueryView.d.ts +64 -0
- package/dist/core/QueryView.js +219 -0
- package/dist/core/StoreManager.d.ts +131 -0
- package/dist/core/StoreManager.js +334 -0
- package/dist/core/ViewRegistry.d.ts +20 -0
- package/dist/core/ViewRegistry.js +55 -0
- package/dist/core/index.d.ts +34 -0
- package/dist/core/index.js +59 -0
- package/dist/core/openIDBWithTimeout.d.ts +27 -0
- package/dist/core/openIDBWithTimeout.js +63 -0
- package/dist/core/query-utils.d.ts +37 -0
- package/dist/core/query-utils.js +60 -0
- package/dist/errors.d.ts +235 -0
- package/dist/errors.js +243 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +82 -0
- package/dist/interfaces/headless.d.ts +95 -0
- package/dist/interfaces/headless.js +41 -0
- package/dist/interfaces/index.d.ts +321 -0
- package/dist/interfaces/index.js +8 -0
- package/dist/mutators/RecordingTransaction.d.ts +36 -0
- package/dist/mutators/RecordingTransaction.js +216 -0
- package/dist/mutators/Transaction.d.ts +48 -0
- package/dist/mutators/Transaction.js +64 -0
- package/dist/mutators/UndoManager.d.ts +114 -0
- package/dist/mutators/UndoManager.js +143 -0
- package/dist/mutators/defineMutators.d.ts +55 -0
- package/dist/mutators/defineMutators.js +28 -0
- package/dist/policy/index.d.ts +19 -0
- package/dist/policy/index.js +18 -0
- package/dist/policy/types.d.ts +74 -0
- package/dist/policy/types.js +17 -0
- package/dist/principal.d.ts +44 -0
- package/dist/principal.js +49 -0
- package/dist/query/client.d.ts +43 -0
- package/dist/query/client.js +84 -0
- package/dist/query/index.d.ts +6 -0
- package/dist/query/index.js +5 -0
- package/dist/query/types.d.ts +143 -0
- package/dist/query/types.js +36 -0
- package/dist/react/AbloProvider.d.ts +205 -0
- package/dist/react/AbloProvider.js +398 -0
- package/dist/react/ClientSideSuspense.d.ts +36 -0
- package/dist/react/ClientSideSuspense.js +17 -0
- package/dist/react/DefaultFallback.d.ts +24 -0
- package/dist/react/DefaultFallback.js +43 -0
- package/dist/react/SyncGroupProvider.d.ts +19 -0
- package/dist/react/SyncGroupProvider.js +44 -0
- package/dist/react/context.d.ts +161 -0
- package/dist/react/context.js +35 -0
- package/dist/react/index.d.ts +64 -0
- package/dist/react/index.js +73 -0
- package/dist/react/internalContext.d.ts +35 -0
- package/dist/react/internalContext.js +3 -0
- package/dist/react/useAblo.d.ts +72 -0
- package/dist/react/useAblo.js +63 -0
- package/dist/react/useCurrentUserId.d.ts +21 -0
- package/dist/react/useCurrentUserId.js +33 -0
- package/dist/react/useErrorListener.d.ts +20 -0
- package/dist/react/useErrorListener.js +39 -0
- package/dist/react/useIntent.d.ts +29 -0
- package/dist/react/useIntent.js +42 -0
- package/dist/react/useMutate.d.ts +83 -0
- package/dist/react/useMutate.js +122 -0
- package/dist/react/useMutationFailureListener.d.ts +26 -0
- package/dist/react/useMutationFailureListener.js +38 -0
- package/dist/react/useMutators.d.ts +56 -0
- package/dist/react/useMutators.js +66 -0
- package/dist/react/usePresence.d.ts +32 -0
- package/dist/react/usePresence.js +41 -0
- package/dist/react/useQuery.d.ts +123 -0
- package/dist/react/useQuery.js +145 -0
- package/dist/react/useReactive.d.ts +35 -0
- package/dist/react/useReactive.js +111 -0
- package/dist/react/useReader.d.ts +69 -0
- package/dist/react/useReader.js +73 -0
- package/dist/react/useSyncStatus.d.ts +61 -0
- package/dist/react/useSyncStatus.js +76 -0
- package/dist/react/useUndoScope.d.ts +36 -0
- package/dist/react/useUndoScope.js +73 -0
- package/dist/realtime/index.d.ts +10 -0
- package/dist/realtime/index.js +9 -0
- package/dist/schema/field.d.ts +134 -0
- package/dist/schema/field.js +264 -0
- package/dist/schema/index.d.ts +29 -0
- package/dist/schema/index.js +38 -0
- package/dist/schema/model.d.ts +326 -0
- package/dist/schema/model.js +89 -0
- package/dist/schema/queries.d.ts +203 -0
- package/dist/schema/queries.js +145 -0
- package/dist/schema/relation.d.ts +172 -0
- package/dist/schema/relation.js +104 -0
- package/dist/schema/schema.d.ts +259 -0
- package/dist/schema/schema.js +188 -0
- package/dist/schema/sugar.d.ts +129 -0
- package/dist/schema/sugar.js +94 -0
- package/dist/source/index.d.ts +423 -0
- package/dist/source/index.js +320 -0
- package/dist/source/pushQueue.d.ts +112 -0
- package/dist/source/pushQueue.js +249 -0
- package/dist/stores/ObjectStore.d.ts +103 -0
- package/dist/stores/ObjectStore.js +371 -0
- package/dist/stores/ObjectStoreContract.d.ts +39 -0
- package/dist/stores/ObjectStoreContract.js +1 -0
- package/dist/stores/SyncActionStore.d.ts +101 -0
- package/dist/stores/SyncActionStore.js +481 -0
- package/dist/sync/BootstrapHelper.d.ts +127 -0
- package/dist/sync/BootstrapHelper.js +434 -0
- package/dist/sync/ConnectionManager.d.ts +136 -0
- package/dist/sync/ConnectionManager.js +465 -0
- package/dist/sync/HydrationCoordinator.d.ts +137 -0
- package/dist/sync/HydrationCoordinator.js +468 -0
- package/dist/sync/NetworkProbe.d.ts +43 -0
- package/dist/sync/NetworkProbe.js +113 -0
- package/dist/sync/OfflineFlush.d.ts +9 -0
- package/dist/sync/OfflineFlush.js +22 -0
- package/dist/sync/OfflineTransactionStore.d.ts +37 -0
- package/dist/sync/OfflineTransactionStore.js +263 -0
- package/dist/sync/SyncWebSocket.d.ts +663 -0
- package/dist/sync/SyncWebSocket.js +1336 -0
- package/dist/sync/createIntentStream.d.ts +33 -0
- package/dist/sync/createIntentStream.js +243 -0
- package/dist/sync/createPresenceStream.d.ts +46 -0
- package/dist/sync/createPresenceStream.js +192 -0
- package/dist/sync/createSnapshot.d.ts +33 -0
- package/dist/sync/createSnapshot.js +124 -0
- package/dist/sync/participants.d.ts +114 -0
- package/dist/sync/participants.js +336 -0
- package/dist/sync/schemas.d.ts +79 -0
- package/dist/sync/schemas.js +78 -0
- package/dist/testing/fixtures/bootstrap.d.ts +45 -0
- package/dist/testing/fixtures/bootstrap.js +53 -0
- package/dist/testing/fixtures/deltas.d.ts +86 -0
- package/dist/testing/fixtures/deltas.js +139 -0
- package/dist/testing/fixtures/models.d.ts +82 -0
- package/dist/testing/fixtures/models.js +270 -0
- package/dist/testing/helpers/react-wrapper.d.ts +66 -0
- package/dist/testing/helpers/react-wrapper.js +64 -0
- package/dist/testing/helpers/sync-engine-harness.d.ts +55 -0
- package/dist/testing/helpers/sync-engine-harness.js +70 -0
- package/dist/testing/helpers/wait.d.ts +25 -0
- package/dist/testing/helpers/wait.js +44 -0
- package/dist/testing/index.d.ts +21 -0
- package/dist/testing/index.js +32 -0
- package/dist/testing/mocks/MockMutationExecutor.d.ts +65 -0
- package/dist/testing/mocks/MockMutationExecutor.js +139 -0
- package/dist/testing/mocks/MockNetworkMonitor.d.ts +20 -0
- package/dist/testing/mocks/MockNetworkMonitor.js +46 -0
- package/dist/testing/mocks/MockSyncContext.d.ts +64 -0
- package/dist/testing/mocks/MockSyncContext.js +100 -0
- package/dist/testing/mocks/MockSyncStore.d.ts +88 -0
- package/dist/testing/mocks/MockSyncStore.js +171 -0
- package/dist/testing/mocks/MockWebSocket.d.ts +66 -0
- package/dist/testing/mocks/MockWebSocket.js +117 -0
- package/dist/transactions/OptimisticEchoTracker.d.ts +82 -0
- package/dist/transactions/OptimisticEchoTracker.js +104 -0
- package/dist/transactions/TransactionQueue.d.ts +499 -0
- package/dist/transactions/TransactionQueue.js +1895 -0
- package/dist/transactions/index.d.ts +16 -0
- package/dist/transactions/index.js +7 -0
- package/dist/transactions/mutation-error-handler.d.ts +5 -0
- package/dist/transactions/mutation-error-handler.js +39 -0
- package/dist/types/global.d.ts +107 -0
- package/dist/types/global.js +38 -0
- package/dist/types/index.d.ts +241 -0
- package/dist/types/index.js +70 -0
- package/dist/types/streams.d.ts +495 -0
- package/dist/types/streams.js +11 -0
- package/dist/utils/asyncIterator.d.ts +41 -0
- package/dist/utils/asyncIterator.js +142 -0
- package/dist/utils/duration.d.ts +28 -0
- package/dist/utils/duration.js +47 -0
- package/dist/utils/mobx-setup.d.ts +42 -0
- package/dist/utils/mobx-setup.js +381 -0
- package/docs/api-keys.md +24 -0
- package/docs/api.md +230 -0
- package/docs/audit.md +81 -0
- package/docs/capabilities.md +163 -0
- package/docs/client-behavior.md +202 -0
- package/docs/data-sources.md +214 -0
- package/docs/examples/agent-human.md +84 -0
- package/docs/examples/ai-sdk-tool.md +92 -0
- package/docs/examples/existing-python-backend.md +249 -0
- package/docs/examples/nextjs.md +88 -0
- package/docs/examples/server-agent.md +86 -0
- package/docs/guarantees.md +148 -0
- package/docs/index.md +97 -0
- package/docs/integration-guide.md +493 -0
- package/docs/interaction-model.md +140 -0
- package/docs/mcp/claude-code.md +43 -0
- package/docs/mcp/cursor.md +53 -0
- package/docs/mcp/windsurf.md +46 -0
- package/docs/mcp.md +59 -0
- package/docs/quickstart.md +152 -0
- package/docs/react.md +115 -0
- package/docs/roadmap.md +45 -0
- package/examples/README.md +54 -0
- package/examples/data-source/README.md +102 -0
- package/examples/data-source/ablo-driver.ts +89 -0
- package/examples/data-source/customer-server.ts +208 -0
- package/examples/data-source/run.ts +101 -0
- package/examples/data-source/schema.ts +25 -0
- package/examples/quickstart.ts +54 -0
- package/examples/tsconfig.json +16 -0
- package/llms.txt +143 -0
- package/package.json +147 -0
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multiplayer stream types.
|
|
3
|
+
*
|
|
4
|
+
* Ablo treats humans and agents as participants on live application
|
|
5
|
+
* entities. Participants announce what they are reading or editing,
|
|
6
|
+
* claim intent before writing, and capture context watermarks before
|
|
7
|
+
* long-running AI work. The customer keeps their own schema, agent
|
|
8
|
+
* stack, tools, prompts, and product policy; the sync engine provides
|
|
9
|
+
* the shared coordination substrate.
|
|
10
|
+
*/
|
|
11
|
+
import type { InferModel, Schema } from '../schema/schema.js';
|
|
12
|
+
/**
|
|
13
|
+
* Any JSON-serializable value. Used where the SDK accepts free-form
|
|
14
|
+
* metadata that will be persisted / transported as JSON — avoids
|
|
15
|
+
* `unknown` drift while preserving flexibility.
|
|
16
|
+
*/
|
|
17
|
+
export type JsonValue = string | number | boolean | null | readonly JsonValue[] | {
|
|
18
|
+
readonly [key: string]: JsonValue;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Identity reference for an actor / on-behalf-of slot. Generic
|
|
22
|
+
* protocol vocabulary; works for sessions, agents, and system roles.
|
|
23
|
+
*/
|
|
24
|
+
export interface ParticipantRef {
|
|
25
|
+
kind: 'user' | 'agent' | 'system';
|
|
26
|
+
id: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Whether the human explicitly approved a change. Open-source
|
|
30
|
+
* consumers that don't track approval keep `auto` as the default.
|
|
31
|
+
*/
|
|
32
|
+
export type ConfirmationState = 'auto' | 'previewed' | 'approved' | 'required_human_approval' | 'auto_historical';
|
|
33
|
+
/**
|
|
34
|
+
* Wire-shape of a single sync delta. Carries the dual-attribution
|
|
35
|
+
* fields (`actor`, `onBehalfOf`, `capabilityId`, `confirmationState`,
|
|
36
|
+
* `causedByTaskId`) that the audit substrate stamps onto each row.
|
|
37
|
+
*/
|
|
38
|
+
export interface AgentDelta {
|
|
39
|
+
id: number;
|
|
40
|
+
actionType: 'I' | 'U' | 'D' | 'A';
|
|
41
|
+
modelName: string;
|
|
42
|
+
modelId: string;
|
|
43
|
+
data: Record<string, unknown>;
|
|
44
|
+
previousData?: Record<string, unknown>;
|
|
45
|
+
/** Who DID the action. */
|
|
46
|
+
actor?: ParticipantRef | null;
|
|
47
|
+
/** On WHOSE AUTHORITY the actor acted. */
|
|
48
|
+
onBehalfOf?: ParticipantRef | null;
|
|
49
|
+
/** Capability that authorized this commit. */
|
|
50
|
+
capabilityId?: string | null;
|
|
51
|
+
/** Whether the human explicitly approved the change. */
|
|
52
|
+
confirmationState?: ConfirmationState | null;
|
|
53
|
+
/** Turn handle that caused this commit. */
|
|
54
|
+
causedByTaskId?: string | null;
|
|
55
|
+
createdAt: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* A reference to whoever's authority bounds a joined participant.
|
|
59
|
+
* The spawned participant can never see or do more than this principal.
|
|
60
|
+
* Enforced cryptographically via Biscuit attenuation.
|
|
61
|
+
*
|
|
62
|
+
* • `SessionRef` — human is joining an agent (chat assistant flow)
|
|
63
|
+
* • `AgentRef` — agent spawning a sub-agent (attenuation chain)
|
|
64
|
+
* • omitted — the API key on the Ablo client is the ceiling
|
|
65
|
+
*/
|
|
66
|
+
export type Principal = SessionRef | AgentRef;
|
|
67
|
+
export interface SessionRef {
|
|
68
|
+
readonly kind: 'session';
|
|
69
|
+
readonly id: string;
|
|
70
|
+
readonly userId: string;
|
|
71
|
+
readonly organizationId: string;
|
|
72
|
+
}
|
|
73
|
+
export interface AgentRef {
|
|
74
|
+
readonly kind: 'agent';
|
|
75
|
+
readonly id: string;
|
|
76
|
+
readonly capabilityToken: string;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Flat snapshot view returned from `participant.snapshot(...)`.
|
|
80
|
+
*
|
|
81
|
+
* - Per-model buckets: `snap.<modelName>[id] → entity` — typed from
|
|
82
|
+
* the schema via `InferModel`, NOT `unknown`. So
|
|
83
|
+
* `snap.clauses[clauseId].text` has `string` (or whatever Zod
|
|
84
|
+
* inferred from the model's shape).
|
|
85
|
+
* - `stamp` — opaque version marker; thread into writes as
|
|
86
|
+
* `{ readAt: snap.stamp }` so the server can reject stale writes.
|
|
87
|
+
* - `signal` — AbortSignal that fires if any captured entity
|
|
88
|
+
* receives a delta during the window. Pass into the LLM call so
|
|
89
|
+
* mid-generation invalidations abort the token stream instead of
|
|
90
|
+
* completing against a dead snapshot.
|
|
91
|
+
* - `onChange(fn)` — callback form for non-abort use cases (logging,
|
|
92
|
+
* UI flags, partial regeneration). Returns an unsubscribe.
|
|
93
|
+
*
|
|
94
|
+
* The per-model buckets collide with the three concurrency fields if
|
|
95
|
+
* you name a model `stamp` / `signal` / `onChange`. We throw at
|
|
96
|
+
* snapshot time with a clear error so the mistake is loud.
|
|
97
|
+
*/
|
|
98
|
+
export type Snapshot<TSchema extends Schema = Schema, ModelName extends keyof TSchema['models'] = keyof TSchema['models']> = {
|
|
99
|
+
readonly stamp: number;
|
|
100
|
+
readonly signal: AbortSignal;
|
|
101
|
+
onChange(listener: (change: ContextChange) => void): () => void;
|
|
102
|
+
} & {
|
|
103
|
+
readonly [M in ModelName]: Readonly<Record<string, InferModel<TSchema, M>>>;
|
|
104
|
+
};
|
|
105
|
+
export interface ContextChange {
|
|
106
|
+
readonly model: string;
|
|
107
|
+
readonly id: string;
|
|
108
|
+
readonly severity: 'semantic' | 'metadata';
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Mutation-time staleness mode. Passed on every write that follows a
|
|
112
|
+
* snapshot. Defaults to `'reject'` when `readAt` is provided without
|
|
113
|
+
* `onStale`.
|
|
114
|
+
*/
|
|
115
|
+
export type OnStaleMode = 'reject' | 'flag' | 'merge' | 'force';
|
|
116
|
+
export interface TargetRange {
|
|
117
|
+
readonly startLine: number;
|
|
118
|
+
readonly endLine: number;
|
|
119
|
+
readonly startColumn?: number;
|
|
120
|
+
readonly endColumn?: number;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* A pointer to one entity, optionally narrowed to a structured
|
|
124
|
+
* subtarget. `type` and `id` are customer schema vocabulary; `path`,
|
|
125
|
+
* `range`, `field`, and `meta` are generic coordination hints for
|
|
126
|
+
* products like code editors, document editors, and design tools.
|
|
127
|
+
*/
|
|
128
|
+
export interface EntityRef {
|
|
129
|
+
readonly type: string;
|
|
130
|
+
readonly id: string;
|
|
131
|
+
readonly path?: string;
|
|
132
|
+
readonly range?: TargetRange;
|
|
133
|
+
readonly field?: string;
|
|
134
|
+
readonly meta?: Record<string, unknown>;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* A pointer to one entity the participant is acting on. Either a
|
|
138
|
+
* typed `EntityRef` (`{ type, id, ... }`), or a tuple
|
|
139
|
+
* `['Clause', 'cl_3']` for ergonomic inline use. The verb methods
|
|
140
|
+
* below accept both.
|
|
141
|
+
*/
|
|
142
|
+
export type PresenceTarget = EntityRef | readonly [type: string, id: string];
|
|
143
|
+
/**
|
|
144
|
+
* Reactive livestream of what every multiplayer participant is doing.
|
|
145
|
+
* Every participant gets one; it's always on, always current.
|
|
146
|
+
*/
|
|
147
|
+
export interface PresenceStream {
|
|
148
|
+
/**
|
|
149
|
+
* This participant's own broadcast state. Mirrors what every other
|
|
150
|
+
* participant sees for this one. Read-only from the owner's side —
|
|
151
|
+
* mutate via `update(...)` below.
|
|
152
|
+
*/
|
|
153
|
+
readonly self: Peer;
|
|
154
|
+
/**
|
|
155
|
+
* Push a new activity to the livestream. Synchronous — the sync
|
|
156
|
+
* engine ships the frame on the already-open WebSocket; there's
|
|
157
|
+
* no request-response round-trip to wait on. Callers update as
|
|
158
|
+
* often as state meaningfully changes (on read, on generate-start,
|
|
159
|
+
* on partial-output, on write, on done).
|
|
160
|
+
*
|
|
161
|
+
* Prefer the verb methods below (`editing`, `viewing`, ...) for
|
|
162
|
+
* canonical actions — they read as one-line sentences and don't
|
|
163
|
+
* force the caller to remember the action-string vocabulary.
|
|
164
|
+
*/
|
|
165
|
+
update(activity: Activity): void;
|
|
166
|
+
/** Participant is actively modifying this entity. */
|
|
167
|
+
editing(target: PresenceTarget, detail?: string): void;
|
|
168
|
+
/** Participant is reading this entity; no modifications. */
|
|
169
|
+
reading(target: PresenceTarget, detail?: string): void;
|
|
170
|
+
/** Participant is reading this entity; no modifications. */
|
|
171
|
+
viewing(target: PresenceTarget, detail?: string): void;
|
|
172
|
+
/** Participant has stepped away from any specific entity. */
|
|
173
|
+
idle(): void;
|
|
174
|
+
/**
|
|
175
|
+
* Reactive view of every OTHER participant's current activity on
|
|
176
|
+
* this participant's sync groups. Reads return the current snapshot;
|
|
177
|
+
* pair with `subscribe(listener)` below to get notified on changes.
|
|
178
|
+
*
|
|
179
|
+
* An LLM pipeline can include `presence.others` in its system prompt
|
|
180
|
+
* so the model literally reasons with knowledge of what other
|
|
181
|
+
* agents are doing right now: "copy-bot is generating a new title
|
|
182
|
+
* for slide 5; don't duplicate that work."
|
|
183
|
+
*/
|
|
184
|
+
readonly others: ReadonlyArray<Peer>;
|
|
185
|
+
/** Subset of `others` filtered to a specific sync group. */
|
|
186
|
+
othersIn(syncGroup: string): ReadonlyArray<Peer>;
|
|
187
|
+
/**
|
|
188
|
+
* Framework-agnostic reactivity primitive. Register a callback that
|
|
189
|
+
* fires every time `others` / `othersIn(...)` content changes (a
|
|
190
|
+
* peer joined, left, or updated its activity). Returns an
|
|
191
|
+
* unsubscribe fn.
|
|
192
|
+
*
|
|
193
|
+
* React binding:
|
|
194
|
+
* ```ts
|
|
195
|
+
* const others = useSyncExternalStore(
|
|
196
|
+
* presence.subscribe,
|
|
197
|
+
* () => presence.others,
|
|
198
|
+
* );
|
|
199
|
+
* ```
|
|
200
|
+
*
|
|
201
|
+
* MobX binding:
|
|
202
|
+
* ```ts
|
|
203
|
+
* autorun(() => {
|
|
204
|
+
* // Triggered on every presence change because the observable
|
|
205
|
+
* // version counter inside presence is read here.
|
|
206
|
+
* const peers = presence.others;
|
|
207
|
+
* // ...
|
|
208
|
+
* });
|
|
209
|
+
* ```
|
|
210
|
+
*/
|
|
211
|
+
subscribe(listener: () => void): () => void;
|
|
212
|
+
/**
|
|
213
|
+
* Async-iterable view of the peer roster. Each iteration yields the
|
|
214
|
+
* current `others` snapshot on every mutation — so the consumer
|
|
215
|
+
* sees the world as it changes without registering a callback.
|
|
216
|
+
*
|
|
217
|
+
* ```ts
|
|
218
|
+
* for await (const peers of participant.presence) {
|
|
219
|
+
* renderAvatars(peers);
|
|
220
|
+
* if (peers.length === 0) break; // iteration stops, subscription drops
|
|
221
|
+
* }
|
|
222
|
+
* ```
|
|
223
|
+
*
|
|
224
|
+
* Each `for await` creates an independent iterator — two loops on
|
|
225
|
+
* the same stream both see every update; they don't steal values
|
|
226
|
+
* from each other. Breaking out of the loop (or throwing) tears
|
|
227
|
+
* down the underlying subscription cleanly via the iterator's
|
|
228
|
+
* `return()` hook.
|
|
229
|
+
*/
|
|
230
|
+
[Symbol.asyncIterator](): AsyncIterableIterator<ReadonlyArray<Peer>>;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* What a participant is currently doing. This is BOTH the SDK and the
|
|
234
|
+
* wire shape — Ablo broadcasts presence on the same WebSocket frame
|
|
235
|
+
* format (`presence_update`) the sync-server has always accepted, so
|
|
236
|
+
* the canonical activity type and the wire activity field are one and
|
|
237
|
+
* the same.
|
|
238
|
+
*
|
|
239
|
+
* Every activity is about a single entity-in-focus. Agents that reason
|
|
240
|
+
* over multiple entities call `presence.update(...)` whenever focus
|
|
241
|
+
* shifts; other participants see the transition in real time.
|
|
242
|
+
*/
|
|
243
|
+
export interface Activity {
|
|
244
|
+
/** Entity type the participant is focused on (e.g. "Slide", "Document"). */
|
|
245
|
+
readonly entityType: string;
|
|
246
|
+
/** Specific entity id. */
|
|
247
|
+
readonly entityId: string;
|
|
248
|
+
/** Optional path for file/document-like targets. */
|
|
249
|
+
readonly path?: string;
|
|
250
|
+
/** Optional line/column range for partial-entity coordination. */
|
|
251
|
+
readonly range?: TargetRange;
|
|
252
|
+
/** Optional field/property path for field-level coordination. */
|
|
253
|
+
readonly field?: string;
|
|
254
|
+
/** App-defined structured metadata. Display-only unless app policy uses it. */
|
|
255
|
+
readonly meta?: Record<string, unknown>;
|
|
256
|
+
/**
|
|
257
|
+
* What the participant is doing to that entity. Canonical values:
|
|
258
|
+
* `'editing'` / `'reviewing'` / `'generating'` / `'analyzing'` /
|
|
259
|
+
* `'executing'`. Free-form strings are accepted for app-specific
|
|
260
|
+
* phases.
|
|
261
|
+
*/
|
|
262
|
+
readonly action: string;
|
|
263
|
+
/** Human-readable detail — "slide 3", "cell A1:B5", etc. */
|
|
264
|
+
readonly detail?: string;
|
|
265
|
+
/**
|
|
266
|
+
* Backpressure signal — load factor in `[0.0, 1.0]`. When set,
|
|
267
|
+
* orchestrator agents reading peer activity can route work around
|
|
268
|
+
* overloaded fleet members. Convention: `0.0` = idle, `1.0` = at
|
|
269
|
+
* capacity, intermediate values = "I have headroom but prefer not."
|
|
270
|
+
* Optional — agents that don't participate in load-aware routing
|
|
271
|
+
* leave this unset; orchestrators ignore them in load calculations.
|
|
272
|
+
* Hub treats it opaquely.
|
|
273
|
+
*/
|
|
274
|
+
readonly loadFactor?: number;
|
|
275
|
+
/**
|
|
276
|
+
* Backpressure gate — explicit signal for new work assignments.
|
|
277
|
+
* Defaults to true when unset (everyone accepts work by default).
|
|
278
|
+
* Set false during graceful shutdown, capacity exhaustion, or when
|
|
279
|
+
* the agent is committed to a long-running step it cannot preempt.
|
|
280
|
+
* Orchestrators MUST treat false as "skip this peer for new work,"
|
|
281
|
+
* and SHOULD treat true with high `loadFactor` as "available but
|
|
282
|
+
* deprioritize."
|
|
283
|
+
*/
|
|
284
|
+
readonly acceptingNewWork?: boolean;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* One participant's live state as seen by everyone else in scope.
|
|
288
|
+
*
|
|
289
|
+
* This is the canonical engine vocabulary. The server's older wire
|
|
290
|
+
* frame still emits `userId` / `isAgent` / `updatedAt`; those names
|
|
291
|
+
* are deprecated and translated at the inbound boundary
|
|
292
|
+
* (`createPresenceStream`) into the names below. New code reads and
|
|
293
|
+
* writes this shape only.
|
|
294
|
+
*/
|
|
295
|
+
export interface Peer {
|
|
296
|
+
readonly participantKind: 'human' | 'agent';
|
|
297
|
+
readonly participantId: string;
|
|
298
|
+
readonly label?: string;
|
|
299
|
+
readonly syncGroups: readonly string[];
|
|
300
|
+
readonly activity: Activity;
|
|
301
|
+
/** Server timestamp of the most recent frame from this participant. */
|
|
302
|
+
readonly lastActive: string;
|
|
303
|
+
/** Pending-mutation intents this participant has declared. */
|
|
304
|
+
readonly activeIntents?: ReadonlyArray<IntentClaim>;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Pending-mutation intent on the wire. Declared via `intent_begin`,
|
|
308
|
+
* cleared on `intent_abandon` / commit / disconnect / TTL expiry.
|
|
309
|
+
* Server stamps `declaredAt` and `expiresAt` (ms epoch). The SDK's
|
|
310
|
+
* `IntentStream.others` exposes a richer `ActiveIntent` view (defined
|
|
311
|
+
* below) that adds `heldBy` so callers know which participant owns it.
|
|
312
|
+
*/
|
|
313
|
+
export interface IntentClaim {
|
|
314
|
+
readonly intentId: string;
|
|
315
|
+
readonly entityType: string;
|
|
316
|
+
readonly entityId: string;
|
|
317
|
+
readonly path?: string;
|
|
318
|
+
readonly range?: TargetRange;
|
|
319
|
+
readonly action: string;
|
|
320
|
+
readonly field?: string;
|
|
321
|
+
readonly meta?: Record<string, unknown>;
|
|
322
|
+
readonly declaredAt: number;
|
|
323
|
+
readonly expiresAt: number;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Transition type carried on every presence frame from the server.
|
|
327
|
+
* - `'enter'` — first frame the receiver sees for this peer.
|
|
328
|
+
* - `'update'` — activity / intent change on an already-known peer.
|
|
329
|
+
* - `'leave'` — peer departed (explicit disconnect or TTL expiry).
|
|
330
|
+
*/
|
|
331
|
+
export type PresenceKind = 'enter' | 'update' | 'leave';
|
|
332
|
+
/** Outbound `presence_update` payload. */
|
|
333
|
+
export interface PresenceUpdatePayload {
|
|
334
|
+
readonly status: 'online' | 'away' | 'offline' | (string & {});
|
|
335
|
+
readonly activity?: Activity;
|
|
336
|
+
readonly isAgent?: boolean;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Intent broadcasts — "I'm about to do X on Y." Broadcasts flow on
|
|
340
|
+
* the same WS as presence, so every participant sees them in real
|
|
341
|
+
* time. Cooperative mutex: the intent doesn't enforce exclusion; it
|
|
342
|
+
* announces. Other agents observe and yield. This is cheaper and
|
|
343
|
+
* more flexible than a central lock table and composes with presence.
|
|
344
|
+
*/
|
|
345
|
+
/**
|
|
346
|
+
* Options common to every verb-style intent announcement
|
|
347
|
+
* (`intents.analyzing`, `.drafting`, etc.).
|
|
348
|
+
*
|
|
349
|
+
* The one required field is the *target* — everything else is a
|
|
350
|
+
* sensible default. Prefer the verb methods in `IntentStream` below
|
|
351
|
+
* (`analyzing(entity, { ttl: '3m' })`) over the raw `announce(...)`
|
|
352
|
+
* escape hatch.
|
|
353
|
+
*/
|
|
354
|
+
export interface IntentOptions {
|
|
355
|
+
/**
|
|
356
|
+
* How long before the server auto-expires this intent if the
|
|
357
|
+
* participant doesn't finish the work. Accepts either a number (in
|
|
358
|
+
* seconds — back-compat with `ttlSeconds`) or a duration string:
|
|
359
|
+
* `'500ms'`, `'30s'`, `'3m'`, `'24h'`.
|
|
360
|
+
*/
|
|
361
|
+
readonly ttl?: Duration;
|
|
362
|
+
}
|
|
363
|
+
/** Re-export of the duration helper shape. See `./duration.ts`. */
|
|
364
|
+
export type Duration = import('../utils/duration.js').Duration;
|
|
365
|
+
export interface ClaimOptions extends IntentOptions {
|
|
366
|
+
/**
|
|
367
|
+
* Free-form reason describing why you're claiming. Surfaces in conflict
|
|
368
|
+
* messages and the activity overlay. Defaults to `'editing'`. Common
|
|
369
|
+
* values: `'editing'`, `'writing'`, `'reviewing'`, custom strings for
|
|
370
|
+
* app-specific phases.
|
|
371
|
+
*/
|
|
372
|
+
readonly reason?: string;
|
|
373
|
+
}
|
|
374
|
+
export interface IntentStream {
|
|
375
|
+
/**
|
|
376
|
+
* Claim an exclusive intent on a target. Returns a handle — call
|
|
377
|
+
* `.revoke()` to cancel, let it expire via TTL, or use `await using`
|
|
378
|
+
* (TC39 explicit resource management) to auto-revoke on scope exit.
|
|
379
|
+
*
|
|
380
|
+
* Server rejects via `intent_rejected` when another participant
|
|
381
|
+
* already holds a claim on the same target. Default `reason` is
|
|
382
|
+
* `'editing'`; pass `{reason: 'writing'}` (or any string) to override.
|
|
383
|
+
*
|
|
384
|
+
* The frame ships on the open WS immediately. One method, one shape —
|
|
385
|
+
* the verb shortcuts (`editing`, `writing`, `announce`) and the
|
|
386
|
+
* scoped `claim(reason, opts)` overload were collapsed into this
|
|
387
|
+
* single primitive.
|
|
388
|
+
*/
|
|
389
|
+
claim(target: PresenceTarget, opts?: ClaimOptions): Claim;
|
|
390
|
+
/**
|
|
391
|
+
* Reactive view of every other participant's active intents.
|
|
392
|
+
* Reads return the current snapshot; pair with `subscribe(...)`
|
|
393
|
+
* below to get notified on change.
|
|
394
|
+
*/
|
|
395
|
+
readonly others: ReadonlyArray<ActiveIntent>;
|
|
396
|
+
/**
|
|
397
|
+
* Framework-agnostic reactivity. Same contract as
|
|
398
|
+
* `PresenceStream.subscribe` — register a listener fired on every
|
|
399
|
+
* change (announce / revoke / TTL expiry received from the server),
|
|
400
|
+
* returns an unsubscribe fn. Use `useSyncExternalStore` in React or
|
|
401
|
+
* `autorun` in MobX.
|
|
402
|
+
*/
|
|
403
|
+
subscribe(listener: () => void): () => void;
|
|
404
|
+
/**
|
|
405
|
+
* Observe server-side intent rejections. Fires when the server
|
|
406
|
+
* rejects an `intents.writing(...)` / `announce(...)` call because
|
|
407
|
+
* another participant already holds an open claim on the same
|
|
408
|
+
* target (cooperative mutex → enforced at the server boundary).
|
|
409
|
+
*
|
|
410
|
+
* Use this to surface conflicts to the user:
|
|
411
|
+
* ```ts
|
|
412
|
+
* participant.intents.onRejected((r) => {
|
|
413
|
+
* toast.error(`${r.heldBy} is editing — try again in a moment`);
|
|
414
|
+
* });
|
|
415
|
+
* ```
|
|
416
|
+
*
|
|
417
|
+
* Returns an unsubscribe fn.
|
|
418
|
+
*/
|
|
419
|
+
onRejected(listener: (rejection: IntentRejection) => void): () => void;
|
|
420
|
+
/**
|
|
421
|
+
* Async-iterable view of everyone else's open intents. Each
|
|
422
|
+
* iteration yields the current snapshot on every mutation.
|
|
423
|
+
*
|
|
424
|
+
* ```ts
|
|
425
|
+
* for await (const openIntents of participant.intents) {
|
|
426
|
+
* if (openIntents.some((i) => i.target.id === clauseId)) wait();
|
|
427
|
+
* }
|
|
428
|
+
* ```
|
|
429
|
+
*/
|
|
430
|
+
[Symbol.asyncIterator](): AsyncIterableIterator<ReadonlyArray<ActiveIntent>>;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Shape of an `intent_rejected` event delivered to
|
|
434
|
+
* `IntentStream.onRejected`. Server rejects an incoming claim when
|
|
435
|
+
* another participant already holds an open intent on the same target.
|
|
436
|
+
*/
|
|
437
|
+
export interface IntentRejection {
|
|
438
|
+
/** The rejected claim's id (the one the caller just tried to mint). */
|
|
439
|
+
readonly intentId: string;
|
|
440
|
+
/** Why the server rejected it — currently always `'conflict'`. */
|
|
441
|
+
readonly reason: 'conflict';
|
|
442
|
+
/** The target that's already held. */
|
|
443
|
+
readonly target: {
|
|
444
|
+
readonly entityType: string;
|
|
445
|
+
readonly entityId: string;
|
|
446
|
+
readonly path?: string;
|
|
447
|
+
readonly range?: TargetRange;
|
|
448
|
+
readonly field?: string;
|
|
449
|
+
readonly meta?: Record<string, unknown>;
|
|
450
|
+
};
|
|
451
|
+
/** Participant id holding the existing claim. */
|
|
452
|
+
readonly heldBy: string;
|
|
453
|
+
/** The existing claim's id (for audit / retry correlation). */
|
|
454
|
+
readonly heldByIntentId: string;
|
|
455
|
+
/** When the existing claim expires (ms since epoch). */
|
|
456
|
+
readonly heldByExpiresAt: number;
|
|
457
|
+
}
|
|
458
|
+
export interface IntentDeclaration {
|
|
459
|
+
readonly target: EntityRef;
|
|
460
|
+
/** Human-readable reason — "rewriting title" / "restyling chart". */
|
|
461
|
+
readonly reason: string;
|
|
462
|
+
/**
|
|
463
|
+
* Expiry — auto-revoke if the participant doesn't finish in time.
|
|
464
|
+
* Number = seconds (back-compat); string = duration (`'3m'`).
|
|
465
|
+
*/
|
|
466
|
+
readonly ttlSeconds?: Duration;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Handle returned from `announce(...)` / `analyzing(...)` / etc.
|
|
470
|
+
*
|
|
471
|
+
* Implements `Symbol.asyncDispose` so callers can write:
|
|
472
|
+
*
|
|
473
|
+
* ```ts
|
|
474
|
+
* {
|
|
475
|
+
* await using work = participant.intents.analyzing(clause, { ttl: '3m' });
|
|
476
|
+
* // ... do the work; intent auto-revokes when the block exits
|
|
477
|
+
* }
|
|
478
|
+
* ```
|
|
479
|
+
*/
|
|
480
|
+
export interface Claim extends AsyncDisposable {
|
|
481
|
+
readonly id: string;
|
|
482
|
+
revoke(): void;
|
|
483
|
+
}
|
|
484
|
+
export interface ActiveIntent extends IntentDeclaration {
|
|
485
|
+
readonly id: string;
|
|
486
|
+
readonly heldBy: string;
|
|
487
|
+
/**
|
|
488
|
+
* Whether the holding participant is a human (session) or an agent.
|
|
489
|
+
* First-class field so UIs can style "agent editing X" differently
|
|
490
|
+
* from "user editing X" without string-parsing `heldBy`.
|
|
491
|
+
*/
|
|
492
|
+
readonly participantKind: 'human' | 'agent';
|
|
493
|
+
readonly announcedAt: string;
|
|
494
|
+
readonly expiresAt: string;
|
|
495
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multiplayer stream types.
|
|
3
|
+
*
|
|
4
|
+
* Ablo treats humans and agents as participants on live application
|
|
5
|
+
* entities. Participants announce what they are reading or editing,
|
|
6
|
+
* claim intent before writing, and capture context watermarks before
|
|
7
|
+
* long-running AI work. The customer keeps their own schema, agent
|
|
8
|
+
* stack, tools, prompts, and product policy; the sync engine provides
|
|
9
|
+
* the shared coordination substrate.
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `asyncIteratorFrom` — adapt any callback-subscription primitive
|
|
3
|
+
* into an async iterable.
|
|
4
|
+
*
|
|
5
|
+
* The inputs are two functions:
|
|
6
|
+
*
|
|
7
|
+
* - `subscribe(onChange): unsubscribe` — the existing reactivity
|
|
8
|
+
* primitive on `PresenceStream` / `IntentStream`. We register a
|
|
9
|
+
* listener that enqueues a value every time the source mutates;
|
|
10
|
+
* we tear it down in `return()`.
|
|
11
|
+
* - `getSnapshot()` — read the latest value to hand to the
|
|
12
|
+
* consumer. Called on every mutation notification.
|
|
13
|
+
*
|
|
14
|
+
* Back-pressure: an unlimited queue. If the consumer is slower than
|
|
15
|
+
* the producer (rare for presence — mutations are <1/s per peer),
|
|
16
|
+
* memory grows monotonically inside the iterator. For the current
|
|
17
|
+
* presence workload this is fine; if we ever surface a high-frequency
|
|
18
|
+
* stream (deltas at full firehose) we can bound the queue or drop
|
|
19
|
+
* coalescable values.
|
|
20
|
+
*
|
|
21
|
+
* Multiple iterators: each call to the returned factory creates an
|
|
22
|
+
* independent iterator with its own subscription. Iterators don't
|
|
23
|
+
* steal values from each other — two `for await` loops on the same
|
|
24
|
+
* stream both observe every mutation.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* Variant of `asyncIteratorFrom` for event-per-iteration streams.
|
|
28
|
+
*
|
|
29
|
+
* Unlike the snapshot variant (where every notification yields the
|
|
30
|
+
* *current value* — coalescing bursts is fine because state is the
|
|
31
|
+
* consumer's concern), this variant yields the *specific value*
|
|
32
|
+
* pushed by each event. Use when the underlying stream delivers
|
|
33
|
+
* discrete events that must not be dropped — e.g. `DeltaEnvelope`
|
|
34
|
+
* firehose.
|
|
35
|
+
*
|
|
36
|
+
* The `subscribe(push): unsubscribe` signature takes a callback that
|
|
37
|
+
* enqueues an event. The consumer's `for await` receives every
|
|
38
|
+
* enqueued value in order.
|
|
39
|
+
*/
|
|
40
|
+
export declare function asyncIteratorFromEvents<T>(subscribe: (push: (value: T) => void) => () => void): AsyncIterableIterator<T>;
|
|
41
|
+
export declare function asyncIteratorFrom<T>(subscribe: (listener: () => void) => () => void, getSnapshot: () => T): AsyncIterableIterator<T>;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `asyncIteratorFrom` — adapt any callback-subscription primitive
|
|
3
|
+
* into an async iterable.
|
|
4
|
+
*
|
|
5
|
+
* The inputs are two functions:
|
|
6
|
+
*
|
|
7
|
+
* - `subscribe(onChange): unsubscribe` — the existing reactivity
|
|
8
|
+
* primitive on `PresenceStream` / `IntentStream`. We register a
|
|
9
|
+
* listener that enqueues a value every time the source mutates;
|
|
10
|
+
* we tear it down in `return()`.
|
|
11
|
+
* - `getSnapshot()` — read the latest value to hand to the
|
|
12
|
+
* consumer. Called on every mutation notification.
|
|
13
|
+
*
|
|
14
|
+
* Back-pressure: an unlimited queue. If the consumer is slower than
|
|
15
|
+
* the producer (rare for presence — mutations are <1/s per peer),
|
|
16
|
+
* memory grows monotonically inside the iterator. For the current
|
|
17
|
+
* presence workload this is fine; if we ever surface a high-frequency
|
|
18
|
+
* stream (deltas at full firehose) we can bound the queue or drop
|
|
19
|
+
* coalescable values.
|
|
20
|
+
*
|
|
21
|
+
* Multiple iterators: each call to the returned factory creates an
|
|
22
|
+
* independent iterator with its own subscription. Iterators don't
|
|
23
|
+
* steal values from each other — two `for await` loops on the same
|
|
24
|
+
* stream both observe every mutation.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* Variant of `asyncIteratorFrom` for event-per-iteration streams.
|
|
28
|
+
*
|
|
29
|
+
* Unlike the snapshot variant (where every notification yields the
|
|
30
|
+
* *current value* — coalescing bursts is fine because state is the
|
|
31
|
+
* consumer's concern), this variant yields the *specific value*
|
|
32
|
+
* pushed by each event. Use when the underlying stream delivers
|
|
33
|
+
* discrete events that must not be dropped — e.g. `DeltaEnvelope`
|
|
34
|
+
* firehose.
|
|
35
|
+
*
|
|
36
|
+
* The `subscribe(push): unsubscribe` signature takes a callback that
|
|
37
|
+
* enqueues an event. The consumer's `for await` receives every
|
|
38
|
+
* enqueued value in order.
|
|
39
|
+
*/
|
|
40
|
+
export function asyncIteratorFromEvents(subscribe) {
|
|
41
|
+
const queue = [];
|
|
42
|
+
const resolvers = [];
|
|
43
|
+
let done = false;
|
|
44
|
+
const push = (value) => {
|
|
45
|
+
if (done)
|
|
46
|
+
return;
|
|
47
|
+
const resolver = resolvers.shift();
|
|
48
|
+
if (resolver) {
|
|
49
|
+
resolver({ value, done: false });
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
queue.push(value);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const unsubscribe = subscribe(push);
|
|
56
|
+
const finish = () => {
|
|
57
|
+
done = true;
|
|
58
|
+
unsubscribe();
|
|
59
|
+
for (const r of resolvers)
|
|
60
|
+
r({ value: undefined, done: true });
|
|
61
|
+
resolvers.length = 0;
|
|
62
|
+
queue.length = 0;
|
|
63
|
+
return { value: undefined, done: true };
|
|
64
|
+
};
|
|
65
|
+
return {
|
|
66
|
+
async next() {
|
|
67
|
+
if (done)
|
|
68
|
+
return { value: undefined, done: true };
|
|
69
|
+
if (queue.length > 0) {
|
|
70
|
+
return { value: queue.shift(), done: false };
|
|
71
|
+
}
|
|
72
|
+
return new Promise((resolve) => {
|
|
73
|
+
resolvers.push(resolve);
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
async return() {
|
|
77
|
+
return finish();
|
|
78
|
+
},
|
|
79
|
+
async throw(err) {
|
|
80
|
+
finish();
|
|
81
|
+
throw err;
|
|
82
|
+
},
|
|
83
|
+
[Symbol.asyncIterator]() {
|
|
84
|
+
return this;
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export function asyncIteratorFrom(subscribe, getSnapshot) {
|
|
89
|
+
const queue = [];
|
|
90
|
+
// Pending `next()` callers waiting for a value. Empty when the
|
|
91
|
+
// consumer is keeping up; holds 0-or-1 resolver when they're
|
|
92
|
+
// awaiting. We never hold more than one at a time — a consumer
|
|
93
|
+
// that calls `next()` twice without awaiting the first breaks
|
|
94
|
+
// the async-iterator contract.
|
|
95
|
+
const resolvers = [];
|
|
96
|
+
let done = false;
|
|
97
|
+
const push = () => {
|
|
98
|
+
if (done)
|
|
99
|
+
return;
|
|
100
|
+
const value = getSnapshot();
|
|
101
|
+
const resolver = resolvers.shift();
|
|
102
|
+
if (resolver) {
|
|
103
|
+
resolver({ value, done: false });
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
queue.push(value);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
const unsubscribe = subscribe(push);
|
|
110
|
+
const finish = () => {
|
|
111
|
+
done = true;
|
|
112
|
+
unsubscribe();
|
|
113
|
+
// Resolve any dangling readers so their awaits don't leak.
|
|
114
|
+
for (const r of resolvers)
|
|
115
|
+
r({ value: undefined, done: true });
|
|
116
|
+
resolvers.length = 0;
|
|
117
|
+
queue.length = 0;
|
|
118
|
+
return { value: undefined, done: true };
|
|
119
|
+
};
|
|
120
|
+
return {
|
|
121
|
+
async next() {
|
|
122
|
+
if (done)
|
|
123
|
+
return { value: undefined, done: true };
|
|
124
|
+
if (queue.length > 0) {
|
|
125
|
+
return { value: queue.shift(), done: false };
|
|
126
|
+
}
|
|
127
|
+
return new Promise((resolve) => {
|
|
128
|
+
resolvers.push(resolve);
|
|
129
|
+
});
|
|
130
|
+
},
|
|
131
|
+
async return() {
|
|
132
|
+
return finish();
|
|
133
|
+
},
|
|
134
|
+
async throw(err) {
|
|
135
|
+
finish();
|
|
136
|
+
throw err;
|
|
137
|
+
},
|
|
138
|
+
[Symbol.asyncIterator]() {
|
|
139
|
+
return this;
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|