@abloatai/ablo 0.24.0 → 0.26.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 +5 -3
- package/CHANGELOG.md +33 -0
- package/README.md +45 -36
- package/dist/BaseSyncedStore.d.ts +72 -195
- package/dist/BaseSyncedStore.js +240 -625
- package/dist/Database.d.ts +8 -9
- package/dist/Database.js +9 -14
- package/dist/LazyReferenceCollection.js +5 -0
- package/dist/Model.js +6 -8
- package/dist/ModelRegistry.d.ts +25 -6
- package/dist/ModelRegistry.js +12 -6
- package/dist/NetworkMonitor.js +1 -1
- package/dist/ObjectPool.js +9 -3
- package/dist/SyncClient.d.ts +15 -5
- package/dist/SyncClient.js +152 -66
- package/dist/SyncEngineContext.d.ts +1 -3
- package/dist/SyncEngineContext.js +1 -2
- package/dist/agent/Agent.d.ts +21 -8
- package/dist/agent/Agent.js +26 -13
- package/dist/ai-sdk/coordination-context.js +9 -3
- package/dist/ai-sdk/wrap.d.ts +1 -1
- package/dist/ai-sdk/wrap.js +1 -1
- package/dist/auth/credentialPolicy.d.ts +10 -2
- package/dist/auth/credentialSource.d.ts +1 -13
- package/dist/auth/credentialSource.js +6 -13
- package/dist/auth/index.js +3 -3
- package/dist/cli.cjs +583 -287
- package/dist/client/Ablo.d.ts +35 -764
- package/dist/client/Ablo.js +64 -688
- package/dist/client/ApiClient.d.ts +14 -1
- package/dist/client/ApiClient.js +63 -9
- package/dist/client/auth.d.ts +8 -17
- package/dist/client/auth.js +42 -6
- package/dist/client/consoleLogger.d.ts +36 -0
- package/dist/client/consoleLogger.js +45 -0
- package/dist/client/createInternalComponents.js +1 -1
- package/dist/client/createModelProxy.js +10 -6
- package/dist/client/credentialEndpoint.d.ts +63 -0
- package/dist/client/credentialEndpoint.js +87 -0
- package/dist/client/hostedEndpoints.d.ts +24 -0
- package/dist/client/hostedEndpoints.js +24 -0
- package/dist/client/httpClient.d.ts +1 -1
- package/dist/client/identity.js +2 -1
- package/dist/client/modelRegistration.d.ts +14 -0
- package/dist/client/modelRegistration.js +321 -0
- package/dist/client/options.d.ts +396 -0
- package/dist/client/options.js +10 -0
- package/dist/client/resourceTypes.d.ts +344 -0
- package/dist/client/resourceTypes.js +13 -0
- package/dist/client/schemaConfig.d.ts +56 -0
- package/dist/client/schemaConfig.js +188 -0
- package/dist/client/sessionMint.d.ts +1 -1
- package/dist/client/validateAbloOptions.js +1 -1
- package/dist/client/wsMutationExecutor.d.ts +33 -0
- package/dist/client/wsMutationExecutor.js +77 -0
- package/dist/context.js +0 -3
- package/dist/coordination/index.d.ts +8 -1
- package/dist/coordination/index.js +24 -1
- package/dist/coordination/trace.d.ts +0 -1
- package/dist/coordination/trace.js +0 -1
- package/dist/core/DatabaseManager.d.ts +0 -1
- package/dist/core/DatabaseManager.js +26 -24
- package/dist/core/QueryProcessor.js +7 -8
- package/dist/core/QueryView.d.ts +14 -2
- package/dist/core/QueryView.js +4 -5
- package/dist/core/StoreManager.d.ts +2 -2
- package/dist/core/StoreManager.js +5 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/openIDBWithTimeout.js +15 -11
- package/dist/core/query-utils.js +1 -1
- package/dist/core/storeContract.d.ts +143 -0
- package/dist/core/storeContract.js +16 -0
- package/dist/errorCodes.d.ts +11 -2
- package/dist/errorCodes.js +155 -137
- package/dist/errors.d.ts +18 -7
- package/dist/errors.js +14 -1
- package/dist/index.js +5 -0
- package/dist/interfaces/index.d.ts +6 -20
- package/dist/keys/index.js +18 -9
- package/dist/mutators/UndoManager.js +6 -1
- package/dist/mutators/defineMutators.d.ts +1 -3
- package/dist/policy/types.d.ts +1 -1
- package/dist/query/client.d.ts +11 -0
- package/dist/query/client.js +81 -52
- package/dist/react/AbloProvider.d.ts +6 -10
- package/dist/react/AbloProvider.js +22 -15
- package/dist/react/context.d.ts +2 -118
- package/dist/react/useAblo.js +1 -1
- package/dist/react/useErrorListener.js +1 -1
- package/dist/react/useMutationFailureListener.js +1 -1
- package/dist/react/useMutators.js +7 -1
- package/dist/react/useReactive.js +0 -1
- package/dist/react/useUndoScope.js +2 -2
- package/dist/schema/ddlLock.d.ts +39 -0
- package/dist/schema/ddlLock.js +51 -0
- package/dist/schema/field.js +1 -1
- package/dist/schema/index.d.ts +4 -3
- package/dist/schema/index.js +15 -5
- package/dist/schema/model.d.ts +3 -3
- package/dist/schema/model.js +2 -2
- package/dist/schema/openapi.js +5 -1
- package/dist/schema/queries.d.ts +1 -1
- package/dist/schema/queries.js +1 -0
- package/dist/schema/residency.d.ts +35 -0
- package/dist/schema/residency.js +27 -0
- package/dist/schema/roles.d.ts +25 -0
- package/dist/schema/roles.js +37 -0
- package/dist/schema/schema.d.ts +3 -3
- package/dist/schema/schema.js +11 -7
- package/dist/schema/serialize.d.ts +8 -8
- package/dist/schema/sync-delta-row.d.ts +2 -2
- package/dist/schema/sync-delta-row.js +2 -2
- package/dist/server/storage-mode.d.ts +8 -1
- package/dist/server/storage-mode.js +14 -4
- package/dist/source/adapter.d.ts +1 -1
- package/dist/source/adapters/drizzle.js +4 -3
- package/dist/source/adapters/kysely.js +4 -3
- package/dist/source/adapters/memory.js +1 -1
- package/dist/source/adapters/prisma.js +5 -4
- package/dist/source/conformance.js +10 -8
- package/dist/source/connector.js +4 -2
- package/dist/source/factory.d.ts +106 -0
- package/dist/source/factory.js +273 -0
- package/dist/source/index.d.ts +11 -461
- package/dist/source/index.js +14 -416
- package/dist/source/next.d.ts +1 -1
- package/dist/source/next.js +1 -1
- package/dist/source/pushQueue.d.ts +1 -1
- package/dist/source/pushQueue.js +6 -3
- package/dist/source/signing.d.ts +59 -0
- package/dist/source/signing.js +142 -0
- package/dist/source/types.d.ts +334 -0
- package/dist/source/types.js +44 -0
- package/dist/stores/ObjectStore.d.ts +1 -1
- package/dist/stores/ObjectStore.js +23 -23
- package/dist/stores/SyncActionStore.d.ts +1 -1
- package/dist/stores/SyncActionStore.js +70 -35
- package/dist/surface.d.ts +1 -1
- package/dist/surface.js +1 -0
- package/dist/sync/AreaOfInterestManager.d.ts +2 -2
- package/dist/sync/AreaOfInterestManager.js +1 -1
- package/dist/sync/BootstrapHelper.d.ts +1 -3
- package/dist/sync/BootstrapHelper.js +29 -8
- package/dist/sync/ConnectionManager.d.ts +8 -0
- package/dist/sync/ConnectionManager.js +19 -4
- package/dist/sync/HydrationCoordinator.d.ts +12 -0
- package/dist/sync/HydrationCoordinator.js +25 -5
- package/dist/sync/NetworkProbe.d.ts +3 -2
- package/dist/sync/NetworkProbe.js +7 -6
- package/dist/sync/SyncWebSocket.d.ts +71 -120
- package/dist/sync/SyncWebSocket.js +324 -630
- package/dist/sync/awaitClaimGrant.js +27 -19
- package/dist/sync/bootstrapApply.d.ts +60 -0
- package/dist/sync/bootstrapApply.js +65 -0
- package/dist/sync/commitFrames.d.ts +43 -0
- package/dist/sync/commitFrames.js +94 -0
- package/dist/sync/createClaimStream.js +3 -2
- package/dist/sync/createSnapshot.d.ts +1 -3
- package/dist/sync/credentialLifecycle.d.ts +175 -0
- package/dist/sync/credentialLifecycle.js +329 -0
- package/dist/sync/deltaPipeline.d.ts +111 -0
- package/dist/sync/deltaPipeline.js +262 -0
- package/dist/sync/groupChange.d.ts +111 -0
- package/dist/sync/groupChange.js +246 -0
- package/dist/sync/heartbeat.d.ts +62 -0
- package/dist/sync/heartbeat.js +91 -0
- package/dist/sync/participants.d.ts +8 -8
- package/dist/sync/syncCursor.d.ts +44 -0
- package/dist/sync/syncCursor.js +59 -0
- package/dist/sync/syncPlan.d.ts +62 -0
- package/dist/sync/syncPlan.js +51 -0
- package/dist/sync/wsFrameHandlers.d.ts +117 -0
- package/dist/sync/wsFrameHandlers.js +378 -0
- package/dist/testing/fixtures/bootstrap.d.ts +9 -11
- package/dist/testing/fixtures/deltas.d.ts +1 -1
- package/dist/testing/fixtures/models.js +1 -0
- package/dist/testing/helpers/sync-engine-harness.d.ts +0 -3
- package/dist/testing/helpers/sync-engine-harness.js +2 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +1 -1
- package/dist/testing/mocks/MockSyncContext.d.ts +1 -17
- package/dist/testing/mocks/MockSyncContext.js +1 -32
- package/dist/testing/mocks/MockSyncStore.d.ts +4 -4
- package/dist/testing/mocks/MockWebSocket.d.ts +2 -2
- package/dist/transactions/TransactionQueue.d.ts +20 -56
- package/dist/transactions/TransactionQueue.js +101 -487
- package/dist/transactions/TransactionStore.d.ts +18 -0
- package/dist/transactions/TransactionStore.js +51 -0
- package/dist/transactions/coalesceRules.d.ts +34 -0
- package/dist/transactions/coalesceRules.js +117 -0
- package/dist/transactions/commitPayload.d.ts +134 -0
- package/dist/transactions/commitPayload.js +152 -0
- package/dist/transactions/deltaConfirmation.d.ts +60 -0
- package/dist/transactions/deltaConfirmation.js +223 -0
- package/dist/transactions/optimistic.d.ts +24 -0
- package/dist/transactions/optimistic.js +45 -0
- package/dist/transactions/persistedReplay.d.ts +93 -0
- package/dist/transactions/persistedReplay.js +105 -0
- package/dist/types/index.d.ts +3 -3
- package/dist/types/index.js +0 -5
- package/dist/types/modelData.d.ts +12 -0
- package/dist/types/modelData.js +11 -0
- package/dist/types/participant.d.ts +21 -0
- package/dist/types/participant.js +12 -0
- package/dist/types/streams.d.ts +8 -14
- package/dist/utils/mobx-setup.d.ts +1 -1
- package/dist/utils/mobx-setup.js +2 -2
- package/dist/wire/errorEnvelope.d.ts +11 -2
- package/dist/wire/errorEnvelope.js +14 -2
- package/dist/wire/frames.d.ts +90 -0
- package/dist/wire/frames.js +55 -1
- package/dist/wire/index.d.ts +4 -1
- package/dist/wire/index.js +20 -1
- package/dist/wire/protocol.d.ts +45 -0
- package/dist/wire/protocol.js +45 -0
- package/dist/wire/protocolVersion.d.ts +56 -0
- package/dist/wire/protocolVersion.js +63 -0
- package/docs/api-keys.md +4 -3
- package/docs/data-sources.md +12 -5
- package/docs/examples/existing-python-backend.md +3 -3
- package/docs/identity.md +4 -4
- package/docs/integration-guide.md +1 -1
- package/docs/react.md +1 -1
- package/docs/sessions.md +5 -7
- package/llms.txt +4 -2
- package/package.json +15 -13
- package/dist/client/index.d.ts +0 -36
- package/dist/client/index.js +0 -33
- package/dist/config/index.d.ts +0 -10
- package/dist/config/index.js +0 -12
- package/dist/interfaces/headless.d.ts +0 -95
- package/dist/interfaces/headless.js +0 -41
- package/dist/query/index.d.ts +0 -6
- package/dist/query/index.js +0 -5
- package/dist/realtime/index.d.ts +0 -10
- package/dist/realtime/index.js +0 -9
- package/dist/schema/plane.d.ts +0 -23
- package/dist/schema/plane.js +0 -19
- package/dist/server/next.d.ts +0 -51
- package/dist/server/next.js +0 -47
- package/dist/sync/OfflineFlush.d.ts +0 -9
- package/dist/sync/OfflineFlush.js +0 -22
- package/dist/sync/OfflineTransactionStore.d.ts +0 -37
- package/dist/sync/OfflineTransactionStore.js +0 -263
- package/dist/transactions/index.d.ts +0 -16
- package/dist/transactions/index.js +0 -7
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared resource-type surface of the client — `ModelRead`, `ModelClient`,
|
|
3
|
+
* the `Commit*` / `Claim*` shapes, the session-mint params/resource, and the
|
|
4
|
+
* `HttpClaimApi` derivation.
|
|
5
|
+
*
|
|
6
|
+
* Extracted from `Ablo.ts` so `ApiClient.ts` / `httpClient.ts` /
|
|
7
|
+
* `sessionMint.ts` can take the wire-facing types WITHOUT importing the
|
|
8
|
+
* factory module that runtime-imports them back (the 4-cycle cluster madge
|
|
9
|
+
* flagged). This module is type-only — ZERO runtime imports — so importing it
|
|
10
|
+
* can never create a cycle. `Ablo.ts` re-exports everything here, so existing
|
|
11
|
+
* import paths keep resolving.
|
|
12
|
+
*/
|
|
13
|
+
import type { StaleNotification, ReadDependency } from '../coordination/schema.js';
|
|
14
|
+
import type { ModelTarget, ModelClaim } from '../coordination/schema.js';
|
|
15
|
+
export type { ModelTarget, ModelClaim };
|
|
16
|
+
import type { SchemaRecord } from '../schema/schema.js';
|
|
17
|
+
import type { SyncGroupInput } from '../schema/roles.js';
|
|
18
|
+
import type { Claim, ClaimStream, ClaimWaitOptions, Duration, HeldClaim } from '../types/streams.js';
|
|
19
|
+
import type { ModelUpdater, ContentionOptions } from './functionalUpdate.js';
|
|
20
|
+
import type { ClaimOptions, ClaimParams, ClaimReadApi, AwaitedClaimMethod, ServerReadOptions } from './createModelProxy.js';
|
|
21
|
+
/**
|
|
22
|
+
* Operations available on each model in the sync engine.
|
|
23
|
+
*
|
|
24
|
+
* Naming aligns with Stripe / OpenAI / Anthropic conventions:
|
|
25
|
+
* `retrieve({ id })` — async single-row server read
|
|
26
|
+
* `list({ where })` — async collection server read
|
|
27
|
+
* `get(id)` / `getAll(...)` / `getCount(...)` — local graph snapshots
|
|
28
|
+
* `create({ data })` / `update({ id, data })` / `delete({ id })` — writes
|
|
29
|
+
* `claim({ id })` — durable claim handle for coordinated writes
|
|
30
|
+
*/
|
|
31
|
+
export type { LocalCountOptions, LocalReadOptions, ModelListScope, ServerReadOptions, ModelRetrieveParams, ModelCreateParams, ModelUpdateParams, ModelDeleteParams, ClaimOptions, ClaimParams, ClaimLookupParams, ClaimReorderParams, Claim, HeldClaim, ModelOperations, } from './createModelProxy.js';
|
|
32
|
+
export type ModelOperationAction = 'create' | 'update' | 'delete' | 'archive' | 'unarchive';
|
|
33
|
+
export type CommitWait = 'queued' | 'confirmed';
|
|
34
|
+
export interface ModelRead<T = Record<string, unknown>> {
|
|
35
|
+
/**
|
|
36
|
+
* The row, or `undefined` when no row matched the id (or it's outside the
|
|
37
|
+
* caller's scope). A miss is data-absence, not an error — `retrieve` never
|
|
38
|
+
* throws "not found", mirroring the WebSocket client's `T | undefined`.
|
|
39
|
+
* Branch on it: `const deal = (await ablo.deals.retrieve({ id })).data; if (!deal) …`.
|
|
40
|
+
*/
|
|
41
|
+
readonly data: T | undefined;
|
|
42
|
+
readonly stamp: number;
|
|
43
|
+
readonly claims: readonly ModelClaim[];
|
|
44
|
+
}
|
|
45
|
+
export type IfClaimedPolicy = 'return' | 'fail';
|
|
46
|
+
export interface ClaimedOptions {
|
|
47
|
+
/**
|
|
48
|
+
* What to do when another participant has claimed the target: `return`
|
|
49
|
+
* includes active claim metadata in the response; `fail` throws
|
|
50
|
+
* `AbloClaimedError`. Waiting for a claim to clear is a claim-side concern —
|
|
51
|
+
* take `ablo.<model>.claim({ id })` (it queues fairly); reads never block.
|
|
52
|
+
*/
|
|
53
|
+
readonly ifClaimed?: IfClaimedPolicy;
|
|
54
|
+
}
|
|
55
|
+
export type { ClaimWaitOptions } from '../types/streams.js';
|
|
56
|
+
export interface ModelReadOptions extends ClaimedOptions {
|
|
57
|
+
}
|
|
58
|
+
export interface ClaimCreateOptions {
|
|
59
|
+
readonly target: ModelTarget;
|
|
60
|
+
/** Human-readable phase shown to peers — `'editing'`, `'writing'`. The same
|
|
61
|
+
* word on every claim surface. */
|
|
62
|
+
readonly reason: string;
|
|
63
|
+
readonly ttl?: Duration;
|
|
64
|
+
/**
|
|
65
|
+
* Join the server's fair FIFO queue when the target is already claimed,
|
|
66
|
+
* rather than failing immediately. `create` then resolves only once the
|
|
67
|
+
* lease is actually ours (the server pushes `claim_acquired` if the target
|
|
68
|
+
* was free, or `claim_granted` when we reach the head of the line). Without
|
|
69
|
+
* this, a contended claim throws. Used by `ablo.<model>.claim` so writers
|
|
70
|
+
* serialize instead of racing.
|
|
71
|
+
*/
|
|
72
|
+
readonly queue?: boolean;
|
|
73
|
+
/** Cap on how long to wait for a queued grant before rejecting. */
|
|
74
|
+
readonly waitTimeoutMs?: number;
|
|
75
|
+
/**
|
|
76
|
+
* Backpressure: reject with `AbloClaimedError('queue_too_deep')` instead of
|
|
77
|
+
* waiting if the queue is already `>= maxQueueDepth` when we join.
|
|
78
|
+
*/
|
|
79
|
+
readonly maxQueueDepth?: number;
|
|
80
|
+
}
|
|
81
|
+
export interface CommitOperationInput {
|
|
82
|
+
readonly action: ModelOperationAction;
|
|
83
|
+
/** The model name — matches `ablo.<model>` and the schema's `model()`. */
|
|
84
|
+
readonly model?: string;
|
|
85
|
+
readonly target?: ModelTarget;
|
|
86
|
+
readonly id?: string | null;
|
|
87
|
+
readonly data?: Record<string, unknown> | null;
|
|
88
|
+
readonly transactionId?: string | null;
|
|
89
|
+
readonly readAt?: number | null;
|
|
90
|
+
readonly onStale?: 'reject' | 'overwrite' | 'notify' | null;
|
|
91
|
+
}
|
|
92
|
+
export interface CommitCreateOptions {
|
|
93
|
+
readonly claimRef?: string | {
|
|
94
|
+
readonly id: string;
|
|
95
|
+
} | null;
|
|
96
|
+
readonly idempotencyKey?: string | null;
|
|
97
|
+
readonly readAt?: number | null;
|
|
98
|
+
readonly onStale?: 'reject' | 'overwrite' | 'notify' | null;
|
|
99
|
+
/**
|
|
100
|
+
* A claim handle from `ablo.<model>.claim({ id })` (or the HTTP claim
|
|
101
|
+
* surface). Same vocabulary as the per-model writes: the handle's
|
|
102
|
+
* snapshot watermark becomes the batch `readAt` default and `onStale`
|
|
103
|
+
* defaults to `'reject'`, so a commit that follows a claim is guarded
|
|
104
|
+
* against concurrent edits without re-stating the watermark by hand.
|
|
105
|
+
* Explicit `readAt`/`onStale` on the options win.
|
|
106
|
+
*/
|
|
107
|
+
readonly claim?: Claim | null;
|
|
108
|
+
readonly operation?: CommitOperationInput;
|
|
109
|
+
readonly operations?: readonly CommitOperationInput[];
|
|
110
|
+
readonly wait?: CommitWait;
|
|
111
|
+
/**
|
|
112
|
+
* Batch-level read dependencies (the STORM "did anything I looked at change?"
|
|
113
|
+
* layer). Declare the rows (`{model,id,readAt,fields?}`) or sync groups
|
|
114
|
+
* (`{group,readAt}`, e.g. `deck:abc`) this batch was premised on; the server
|
|
115
|
+
* validates none moved since `readAt` and fires the entry's `onStale` over the
|
|
116
|
+
* batch. Distinct from the write-target `readAt` — this guards what you READ,
|
|
117
|
+
* not what you write.
|
|
118
|
+
*/
|
|
119
|
+
readonly reads?: readonly ReadDependency[] | null;
|
|
120
|
+
}
|
|
121
|
+
export interface CommitReceipt {
|
|
122
|
+
readonly id: string;
|
|
123
|
+
readonly status: CommitWait;
|
|
124
|
+
readonly lastSyncId?: number;
|
|
125
|
+
/**
|
|
126
|
+
* Stale-context notifications (notify-instead-of-abort, non-coercion). Present
|
|
127
|
+
* only when this commit guarded a write with `onStale: 'notify' and
|
|
128
|
+
* the premise moved concurrently — the conflicting field's current value,
|
|
129
|
+
* handed back as data instead of a forced `AbloStaleContextError`. The engine
|
|
130
|
+
* surfaces state; the intelligent actor (agent or human) decides how to
|
|
131
|
+
* resolve. Also fires on `conflict:notified`.
|
|
132
|
+
*/
|
|
133
|
+
readonly notifications?: readonly StaleNotification[];
|
|
134
|
+
/**
|
|
135
|
+
* Ids of UPDATE/DELETE targets in this commit that matched ZERO rows (the row
|
|
136
|
+
* doesn't exist, or is outside the caller's org). Present (non-empty) only
|
|
137
|
+
* when a write missed. Typed resource wrappers turn this into a loud
|
|
138
|
+
* `AbloNotFoundError`; a raw `commits.create` caller can inspect it directly.
|
|
139
|
+
*/
|
|
140
|
+
readonly missingIds?: readonly string[];
|
|
141
|
+
}
|
|
142
|
+
export interface CommitResource {
|
|
143
|
+
create(options: CommitCreateOptions): Promise<CommitReceipt>;
|
|
144
|
+
}
|
|
145
|
+
export interface ClaimResource extends ClaimStream {
|
|
146
|
+
create(options: ClaimCreateOptions): Promise<Claim>;
|
|
147
|
+
list(target?: Partial<ModelTarget>): readonly ModelClaim[];
|
|
148
|
+
waitFor(target: Partial<ModelTarget>, options?: ClaimWaitOptions): Promise<void>;
|
|
149
|
+
}
|
|
150
|
+
export interface ModelMutationOptions extends ClaimedOptions {
|
|
151
|
+
readonly claimRef?: string | {
|
|
152
|
+
readonly id: string;
|
|
153
|
+
} | null;
|
|
154
|
+
readonly idempotencyKey?: string | null;
|
|
155
|
+
readonly readAt?: number | null;
|
|
156
|
+
readonly onStale?: 'reject' | 'overwrite' | 'notify' | null;
|
|
157
|
+
readonly wait?: CommitWait;
|
|
158
|
+
readonly claim?: Claim | ClaimOptions | null;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* The HTTP/stateless claim surface. Normal tools usually put `claim` directly
|
|
162
|
+
* on the write (`update({ id, data, claim })`) and let the SDK release it. Use
|
|
163
|
+
* this namespace for multi-step handles and coordination screens.
|
|
164
|
+
*
|
|
165
|
+
* Same surface as the reactive {@link ClaimApi}, but every read is a server
|
|
166
|
+
* round-trip, so `state`/`queue`/`reorder` are **awaited** here (the WebSocket
|
|
167
|
+
* client resolves them synchronously from its local pool — which is what lets
|
|
168
|
+
* `useAblo((ablo) => ablo.x.claim.state({ id }))` work inside a React render; a
|
|
169
|
+
* stateless client has no pool to read, so the `Promise` is unavoidable).
|
|
170
|
+
*
|
|
171
|
+
* Mechanically DERIVED from `ClaimReadApi` via {@link AwaitedClaimMethod} so the
|
|
172
|
+
* two transports can never drift: the ONLY difference is the uniform `Promise`
|
|
173
|
+
* wrapper that statelessness forces. `claim({ id })` is identical (already async
|
|
174
|
+
* on both); `state`/`queue`/`reorder`/`release` are the awaited form.
|
|
175
|
+
*/
|
|
176
|
+
export type HttpClaimApi<T = Record<string, unknown>> = ((params: ClaimParams<T>) => Promise<HeldClaim<T>>) & {
|
|
177
|
+
[K in keyof ClaimReadApi<T>]: AwaitedClaimMethod<ClaimReadApi<T>[K]>;
|
|
178
|
+
};
|
|
179
|
+
export interface ModelClient<T = Record<string, unknown>> {
|
|
180
|
+
/**
|
|
181
|
+
* Single-row read over HTTP. **Returns an envelope, not the bare row** — the
|
|
182
|
+
* row is on `.data`, alongside the `.stamp` watermark (for stale-context
|
|
183
|
+
* guards on the following write) and any active `.claims`. A stateless HTTP
|
|
184
|
+
* client can't synthesize the watermark from a local snapshot, so the
|
|
185
|
+
* envelope is load-bearing here (the WebSocket client's `retrieve` returns
|
|
186
|
+
* `T | undefined` because it reads from the hydrated pool).
|
|
187
|
+
*
|
|
188
|
+
* ```ts
|
|
189
|
+
* const deal = await ablo.deals.retrieve({ id });
|
|
190
|
+
* deal.data?.recommendation; // ← the row is on .data
|
|
191
|
+
* deal.stamp; // watermark — pass to the next write's readAt
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
retrieve(params: ModelReadOptions & {
|
|
195
|
+
readonly id: string;
|
|
196
|
+
}): Promise<ModelRead<T>>;
|
|
197
|
+
/**
|
|
198
|
+
* Collection read over HTTP (server round-trip). Equality `where`, `orderBy`,
|
|
199
|
+
* `limit`. Present on the stateless protocol client; the store-backed
|
|
200
|
+
* `.model(name)` accessor omits it (use the typed `ablo.<model>.list` there).
|
|
201
|
+
*/
|
|
202
|
+
list?(options?: ServerReadOptions<T>): Promise<T[]>;
|
|
203
|
+
/**
|
|
204
|
+
* Create a row and return it — the confirmed, authoritative server row (with
|
|
205
|
+
* framework defaults like `createdAt`/`createdBy`), mirroring the WebSocket
|
|
206
|
+
* client's `create`. A re-create of an existing caller-supplied id is
|
|
207
|
+
* idempotent and returns the EXISTING row, not the input.
|
|
208
|
+
*/
|
|
209
|
+
create(params: ModelMutationOptions & {
|
|
210
|
+
readonly data: Record<string, unknown>;
|
|
211
|
+
readonly id?: string | null;
|
|
212
|
+
}): Promise<T>;
|
|
213
|
+
update(params: ModelMutationOptions & {
|
|
214
|
+
readonly id: string;
|
|
215
|
+
readonly data: Record<string, unknown>;
|
|
216
|
+
}): Promise<CommitReceipt>;
|
|
217
|
+
/**
|
|
218
|
+
* Update under contention with a function of the latest state —
|
|
219
|
+
* `update(id, current => next)`, the `setState(prev => next)` of the data
|
|
220
|
+
* layer. The SDK reads the freshest row, runs your updater, writes it as a
|
|
221
|
+
* compare-and-swap against the row's watermark, and re-reads + re-runs on any
|
|
222
|
+
* concurrent write. No claim, no identity, no conflict codes surface: the
|
|
223
|
+
* write either lands or throws {@link AbloContentionError} once its reconcile
|
|
224
|
+
* budget is spent. Return `null`/`undefined` from the updater to skip the
|
|
225
|
+
* write (resolves to `undefined`).
|
|
226
|
+
*/
|
|
227
|
+
update(id: string, updater: ModelUpdater<T>, options?: ContentionOptions): Promise<CommitReceipt | undefined>;
|
|
228
|
+
delete(params: ModelMutationOptions & {
|
|
229
|
+
readonly id: string;
|
|
230
|
+
}): Promise<CommitReceipt>;
|
|
231
|
+
/**
|
|
232
|
+
* Durable lease + FIFO wait-line over HTTP — coordination without a socket.
|
|
233
|
+
* Present on the stateless protocol client (`Ablo({ schema: null })` /
|
|
234
|
+
* `createAbloHttpClient`); the store-backed `.model(name)` accessor omits it
|
|
235
|
+
* (the typed `ablo.<model>.claim` proxy is the full reactive namespace there).
|
|
236
|
+
*/
|
|
237
|
+
claim?: HttpClaimApi<T>;
|
|
238
|
+
}
|
|
239
|
+
/** A single data operation a scoped **agent** session may perform on a model. */
|
|
240
|
+
export type SessionOperation = 'read' | 'create' | 'update' | 'delete';
|
|
241
|
+
/** Mint params for an **end-user** session — full data authority within the
|
|
242
|
+
* org (the Stripe `ephemeralKeys.create` / Supabase session shape). Mints an
|
|
243
|
+
* `ek_` token. `user.id` is your end user's external IdP id (becomes the
|
|
244
|
+
* session's `participantId`); Ablo does not model your users, so it's an
|
|
245
|
+
* honest string at the trust boundary. */
|
|
246
|
+
export interface CreateUserSessionParams {
|
|
247
|
+
/** Your end user. `id` becomes the token's `participantId`. */
|
|
248
|
+
user: {
|
|
249
|
+
id: string;
|
|
250
|
+
};
|
|
251
|
+
/** Mint the session into THIS organization instead of the key's own org — the
|
|
252
|
+
* Stripe Connect `Stripe-Account` pattern, for a platform serving many tenants
|
|
253
|
+
* from one backend. Requires the `sk_` to carry the `ephemeral:mint-any-org`
|
|
254
|
+
* scope; omit for the normal single-tenant case. */
|
|
255
|
+
organizationId?: string;
|
|
256
|
+
/** Sync groups this session may subscribe to — typed (`'default'` or
|
|
257
|
+
* `<namespace>:<id>`; build with `syncGroup(kind, id)` from
|
|
258
|
+
* `@abloatai/ablo/schema`). Omit for the server default:
|
|
259
|
+
* `[org:<your org>, user:<user.id>]`. */
|
|
260
|
+
syncGroups?: readonly SyncGroupInput[];
|
|
261
|
+
/** Token lifetime in seconds. Defaults to 900 (15m, the Stripe ephemeral default). */
|
|
262
|
+
ttlSeconds?: number;
|
|
263
|
+
/** Opaque identity blob echoed back to the client as `ablo.user`. */
|
|
264
|
+
userMeta?: Record<string, unknown>;
|
|
265
|
+
agent?: never;
|
|
266
|
+
can?: never;
|
|
267
|
+
}
|
|
268
|
+
/** Mint params for a scoped **agent** session — mints a restricted `rk_` token
|
|
269
|
+
* gated to exactly the operations named in `can`. `can` is typed off your
|
|
270
|
+
* schema (no magic `'task.update'` strings): `{ Task: ['update'], Deck: ['read'] }`
|
|
271
|
+
* — the SDK serializes each entry to the wire allowlist (`task.update`). */
|
|
272
|
+
export interface CreateAgentSessionParams<S extends SchemaRecord> {
|
|
273
|
+
/** Your agent. `id` becomes the token's `participantId`. */
|
|
274
|
+
agent: {
|
|
275
|
+
id: string;
|
|
276
|
+
};
|
|
277
|
+
/** Per-model operation allowlist, typed against the schema's model names. */
|
|
278
|
+
can: Partial<Record<keyof S & string, readonly SessionOperation[]>>;
|
|
279
|
+
/** Sync groups this session may subscribe to — typed (`'default'` or
|
|
280
|
+
* `<namespace>:<id>`; build with `syncGroup(kind, id)` from
|
|
281
|
+
* `@abloatai/ablo/schema`). Omit for the server default: the org
|
|
282
|
+
* anchor (`org:<your org>`) + the agent's own anchor. */
|
|
283
|
+
syncGroups?: readonly SyncGroupInput[];
|
|
284
|
+
/** Token lifetime in seconds. Defaults to 900 (15m, the Stripe ephemeral default). */
|
|
285
|
+
ttlSeconds?: number;
|
|
286
|
+
/** Opaque identity blob echoed back to the client as `ablo.agent`. */
|
|
287
|
+
userMeta?: Record<string, unknown>;
|
|
288
|
+
user?: never;
|
|
289
|
+
}
|
|
290
|
+
/** Params for {@link Ablo.sessions}.create — a discriminated union: pass
|
|
291
|
+
* `{ user }` for a full-authority end-user session (`ek_`) or `{ agent, can }`
|
|
292
|
+
* for a scoped agent session (`rk_`). */
|
|
293
|
+
export type CreateSessionParams<S extends SchemaRecord> = CreateUserSessionParams | CreateAgentSessionParams<S>;
|
|
294
|
+
/** Params for {@link Ablo.agents}.create — a flattened agent descriptor (no
|
|
295
|
+
* `{ agent }` discriminator: `agents.create` only ever mints an agent). Unlike
|
|
296
|
+
* {@link CreateSessionParams} it resolves to a connected, scoped {@link Ablo}
|
|
297
|
+
* client rather than a raw token. */
|
|
298
|
+
export interface CreateAgentClientParams<S extends SchemaRecord> {
|
|
299
|
+
/** Wire participant identity (`agent:<id>`) — what claim exclusion and the
|
|
300
|
+
* FIFO queue gate on. OMIT to get a fresh `crypto.randomUUID()`: a distinct,
|
|
301
|
+
* independent participant (the default, and what you want for concurrent
|
|
302
|
+
* agents). Pass a STABLE string only when one logical agent must re-attach
|
|
303
|
+
* to its own held claims across reconnects/restarts. */
|
|
304
|
+
id?: string;
|
|
305
|
+
/** Human-readable label for logs / attribution (carried in `userMeta.name`).
|
|
306
|
+
* INDEPENDENT of `id`: two agents that share a `name` still receive distinct
|
|
307
|
+
* ids and coordinate as SEPARATE participants — `name` never derives or
|
|
308
|
+
* collapses identity. */
|
|
309
|
+
name?: string;
|
|
310
|
+
/** Per-model operation allowlist, typed against the schema's model names. */
|
|
311
|
+
can: Partial<Record<keyof S & string, readonly SessionOperation[]>>;
|
|
312
|
+
/** Sync groups this agent may subscribe to — typed (`'default'` or
|
|
313
|
+
* `<namespace>:<id>`). Omit for the server default (org anchor + the
|
|
314
|
+
* agent's own anchor). */
|
|
315
|
+
syncGroups?: readonly SyncGroupInput[];
|
|
316
|
+
/** Token lifetime in seconds. Defaults to 900 (15m); the returned client
|
|
317
|
+
* auto-re-mints before expiry, so a long-running agent never handles
|
|
318
|
+
* rotation itself. */
|
|
319
|
+
ttlSeconds?: number;
|
|
320
|
+
/** Extra opaque identity blob echoed on the session scope. Merged with
|
|
321
|
+
* `name` (the `name` param wins if you also set `userMeta.name`). */
|
|
322
|
+
userMeta?: Record<string, unknown>;
|
|
323
|
+
}
|
|
324
|
+
/** A minted session token — the Stripe ephemeral-key / Supabase session
|
|
325
|
+
* resource. `token` is the secret the holder presents as its bearer. */
|
|
326
|
+
export interface AbloSession {
|
|
327
|
+
object: 'session';
|
|
328
|
+
/** Stable id of the minted credential (for revocation). */
|
|
329
|
+
id: string;
|
|
330
|
+
/** The short-lived session token — `ek_` for a `{ user }` session, `rk_`
|
|
331
|
+
* for an `{ agent }` session. Hand this to the participant's runtime. */
|
|
332
|
+
token: string;
|
|
333
|
+
/** ISO-8601 expiry. */
|
|
334
|
+
expiresAt: string;
|
|
335
|
+
organizationId: string;
|
|
336
|
+
scope: {
|
|
337
|
+
organizationId: string;
|
|
338
|
+
syncGroups: readonly string[];
|
|
339
|
+
operations: readonly string[];
|
|
340
|
+
participantKind: 'user' | 'agent' | 'system';
|
|
341
|
+
participantId: string;
|
|
342
|
+
};
|
|
343
|
+
userMeta: Record<string, unknown>;
|
|
344
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared resource-type surface of the client — `ModelRead`, `ModelClient`,
|
|
3
|
+
* the `Commit*` / `Claim*` shapes, the session-mint params/resource, and the
|
|
4
|
+
* `HttpClaimApi` derivation.
|
|
5
|
+
*
|
|
6
|
+
* Extracted from `Ablo.ts` so `ApiClient.ts` / `httpClient.ts` /
|
|
7
|
+
* `sessionMint.ts` can take the wire-facing types WITHOUT importing the
|
|
8
|
+
* factory module that runtime-imports them back (the 4-cycle cluster madge
|
|
9
|
+
* flagged). This module is type-only — ZERO runtime imports — so importing it
|
|
10
|
+
* can never create a cycle. `Ablo.ts` re-exports everything here, so existing
|
|
11
|
+
* import paths keep resolving.
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema-derived engine config — `computeFKDepthPriority` (the Tarjan-SCC
|
|
3
|
+
* create-order derivation) and `deriveConfigFromSchema` (the `SyncEngineConfig`
|
|
4
|
+
* the factory seeds the DI context with).
|
|
5
|
+
*
|
|
6
|
+
* Extracted from `Ablo.ts` as a pure leaf: both functions are deterministic
|
|
7
|
+
* schema → value derivations with no engine state.
|
|
8
|
+
*/
|
|
9
|
+
import type { Schema } from '../schema/schema.js';
|
|
10
|
+
import type { SyncEngineConfig } from '../interfaces/index.js';
|
|
11
|
+
/**
|
|
12
|
+
* Compute a create-priority map from schema `belongsTo` relations using
|
|
13
|
+
* Tarjan's strongly-connected-components algorithm.
|
|
14
|
+
*
|
|
15
|
+
* The FK graph has an edge `child → parent` for every `belongsTo`. Tarjan
|
|
16
|
+
* runs a single linear DFS that simultaneously (a) detects cycles by
|
|
17
|
+
* grouping mutually-reachable nodes into SCCs and (b) emits those SCCs
|
|
18
|
+
* in reverse topological order of the condensation graph. In this edge
|
|
19
|
+
* convention a "sink" SCC has no outgoing edges — i.e. no parents — so
|
|
20
|
+
* it is an *FK root* (`organizations`, `themes`, etc.). Tarjan emits
|
|
21
|
+
* roots first and leaves last, exactly the order in which rows must be
|
|
22
|
+
* inserted to satisfy FK constraints.
|
|
23
|
+
*
|
|
24
|
+
* Priorities are assigned by emit order: SCC #0 → 10, SCC #1 → 20, …
|
|
25
|
+
* Members of the same SCC share a priority, so insertion order wins the
|
|
26
|
+
* tiebreak inside a cycle (this matters for cyclic schemas like
|
|
27
|
+
* `slideDecks ↔ layouts`, where one direction is the user's chosen
|
|
28
|
+
* "soft" edge — only the consumer's mutator sequence knows which one).
|
|
29
|
+
*
|
|
30
|
+
* This algorithm is iteration-order-independent: starting the DFS from
|
|
31
|
+
* any node yields the same SCC partitioning, and SCCs always come out
|
|
32
|
+
* in valid topological order. The previous DFS-with-memoization
|
|
33
|
+
* heuristic broke under cycles by treating the back-edge as depth 0,
|
|
34
|
+
* which made priorities depend on which node the walk happened to
|
|
35
|
+
* enter the cycle at.
|
|
36
|
+
*
|
|
37
|
+
* Schema authors can mark one side of a cycle with
|
|
38
|
+
* `belongsTo(target, fk, { defer: true })`. Those edges are excluded
|
|
39
|
+
* from the dependency graph entirely, which deterministically breaks
|
|
40
|
+
* the cycle and turns the SCC into a chain — the marked child gets a
|
|
41
|
+
* strictly higher priority than its parent instead of being tied with
|
|
42
|
+
* it. Pair with a Postgres `DEFERRABLE INITIALLY DEFERRED` constraint
|
|
43
|
+
* if you want the database side of the cycle to also relax. See
|
|
44
|
+
* {@link BelongsToOptions.defer}.
|
|
45
|
+
*
|
|
46
|
+
* The returned map is keyed by {@link ModelDef.typename} (falling back
|
|
47
|
+
* to the schema key), because that is what `Model.getModelName()`
|
|
48
|
+
* returns at transaction time — keying by schema key would silently
|
|
49
|
+
* miss the lookup and every model would fall through to
|
|
50
|
+
* `defaultCreatePriority`.
|
|
51
|
+
*
|
|
52
|
+
* Reference: Tarjan, R. (1972), "Depth-first search and linear graph
|
|
53
|
+
* algorithms." Linear in V + E.
|
|
54
|
+
*/
|
|
55
|
+
export declare function computeFKDepthPriority(schema: Schema): ReadonlyMap<string, number>;
|
|
56
|
+
export declare function deriveConfigFromSchema(schema: Schema): SyncEngineConfig;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema-derived engine config — `computeFKDepthPriority` (the Tarjan-SCC
|
|
3
|
+
* create-order derivation) and `deriveConfigFromSchema` (the `SyncEngineConfig`
|
|
4
|
+
* the factory seeds the DI context with).
|
|
5
|
+
*
|
|
6
|
+
* Extracted from `Ablo.ts` as a pure leaf: both functions are deterministic
|
|
7
|
+
* schema → value derivations with no engine state.
|
|
8
|
+
*/
|
|
9
|
+
import { schemaHash } from '../schema/serialize.js';
|
|
10
|
+
// ── Config derivation from schema ─────────────────────────────────────────
|
|
11
|
+
/**
|
|
12
|
+
* Compute a create-priority map from schema `belongsTo` relations using
|
|
13
|
+
* Tarjan's strongly-connected-components algorithm.
|
|
14
|
+
*
|
|
15
|
+
* The FK graph has an edge `child → parent` for every `belongsTo`. Tarjan
|
|
16
|
+
* runs a single linear DFS that simultaneously (a) detects cycles by
|
|
17
|
+
* grouping mutually-reachable nodes into SCCs and (b) emits those SCCs
|
|
18
|
+
* in reverse topological order of the condensation graph. In this edge
|
|
19
|
+
* convention a "sink" SCC has no outgoing edges — i.e. no parents — so
|
|
20
|
+
* it is an *FK root* (`organizations`, `themes`, etc.). Tarjan emits
|
|
21
|
+
* roots first and leaves last, exactly the order in which rows must be
|
|
22
|
+
* inserted to satisfy FK constraints.
|
|
23
|
+
*
|
|
24
|
+
* Priorities are assigned by emit order: SCC #0 → 10, SCC #1 → 20, …
|
|
25
|
+
* Members of the same SCC share a priority, so insertion order wins the
|
|
26
|
+
* tiebreak inside a cycle (this matters for cyclic schemas like
|
|
27
|
+
* `slideDecks ↔ layouts`, where one direction is the user's chosen
|
|
28
|
+
* "soft" edge — only the consumer's mutator sequence knows which one).
|
|
29
|
+
*
|
|
30
|
+
* This algorithm is iteration-order-independent: starting the DFS from
|
|
31
|
+
* any node yields the same SCC partitioning, and SCCs always come out
|
|
32
|
+
* in valid topological order. The previous DFS-with-memoization
|
|
33
|
+
* heuristic broke under cycles by treating the back-edge as depth 0,
|
|
34
|
+
* which made priorities depend on which node the walk happened to
|
|
35
|
+
* enter the cycle at.
|
|
36
|
+
*
|
|
37
|
+
* Schema authors can mark one side of a cycle with
|
|
38
|
+
* `belongsTo(target, fk, { defer: true })`. Those edges are excluded
|
|
39
|
+
* from the dependency graph entirely, which deterministically breaks
|
|
40
|
+
* the cycle and turns the SCC into a chain — the marked child gets a
|
|
41
|
+
* strictly higher priority than its parent instead of being tied with
|
|
42
|
+
* it. Pair with a Postgres `DEFERRABLE INITIALLY DEFERRED` constraint
|
|
43
|
+
* if you want the database side of the cycle to also relax. See
|
|
44
|
+
* {@link BelongsToOptions.defer}.
|
|
45
|
+
*
|
|
46
|
+
* The returned map is keyed by {@link ModelDef.typename} (falling back
|
|
47
|
+
* to the schema key), because that is what `Model.getModelName()`
|
|
48
|
+
* returns at transaction time — keying by schema key would silently
|
|
49
|
+
* miss the lookup and every model would fall through to
|
|
50
|
+
* `defaultCreatePriority`.
|
|
51
|
+
*
|
|
52
|
+
* Reference: Tarjan, R. (1972), "Depth-first search and linear graph
|
|
53
|
+
* algorithms." Linear in V + E.
|
|
54
|
+
*/
|
|
55
|
+
export function computeFKDepthPriority(schema) {
|
|
56
|
+
// schemaKey → typename (wire name used at transaction time)
|
|
57
|
+
const keyToTypename = new Map();
|
|
58
|
+
for (const [key, def] of Object.entries(schema.models)) {
|
|
59
|
+
keyToTypename.set(key, def.typename ?? key);
|
|
60
|
+
}
|
|
61
|
+
// Adjacency: schemaKey → parent schema keys pulled from `belongsTo`.
|
|
62
|
+
// Parents not in the schema (e.g. external types) are dropped so the
|
|
63
|
+
// graph stays closed. Edges marked `{ defer: true }` are also
|
|
64
|
+
// dropped — the schema author has declared this side of a cycle to
|
|
65
|
+
// be the "soft" one (insert with null FK, patch later), so the
|
|
66
|
+
// dependency-graph walker treats it as if the edge weren't there.
|
|
67
|
+
// That breaks the cycle deterministically and lets the other side
|
|
68
|
+
// become a strict topological predecessor.
|
|
69
|
+
const parentsOf = new Map();
|
|
70
|
+
for (const [key, def] of Object.entries(schema.models)) {
|
|
71
|
+
const out = [];
|
|
72
|
+
for (const rel of Object.values(def.relations)) {
|
|
73
|
+
if (rel.type !== 'belongsTo')
|
|
74
|
+
continue;
|
|
75
|
+
if (!keyToTypename.has(rel.target))
|
|
76
|
+
continue;
|
|
77
|
+
if (rel.options?.defer === true)
|
|
78
|
+
continue;
|
|
79
|
+
out.push(rel.target);
|
|
80
|
+
}
|
|
81
|
+
parentsOf.set(key, out);
|
|
82
|
+
}
|
|
83
|
+
// Tarjan SCC bookkeeping
|
|
84
|
+
const dfsIndex = new Map();
|
|
85
|
+
const lowlink = new Map();
|
|
86
|
+
const onStack = new Set();
|
|
87
|
+
const stack = [];
|
|
88
|
+
const sccs = [];
|
|
89
|
+
let counter = 0;
|
|
90
|
+
function strongconnect(v) {
|
|
91
|
+
dfsIndex.set(v, counter);
|
|
92
|
+
lowlink.set(v, counter);
|
|
93
|
+
counter++;
|
|
94
|
+
stack.push(v);
|
|
95
|
+
onStack.add(v);
|
|
96
|
+
for (const w of parentsOf.get(v) ?? []) {
|
|
97
|
+
if (!dfsIndex.has(w)) {
|
|
98
|
+
strongconnect(w);
|
|
99
|
+
lowlink.set(v, Math.min(lowlink.get(v), lowlink.get(w)));
|
|
100
|
+
}
|
|
101
|
+
else if (onStack.has(w)) {
|
|
102
|
+
// Back-edge into the active DFS path — w is in the same SCC as v.
|
|
103
|
+
lowlink.set(v, Math.min(lowlink.get(v), dfsIndex.get(w)));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// v is the root of an SCC: pop everything down to v inclusive.
|
|
107
|
+
if (lowlink.get(v) === dfsIndex.get(v)) {
|
|
108
|
+
const component = [];
|
|
109
|
+
let w;
|
|
110
|
+
do {
|
|
111
|
+
w = stack.pop();
|
|
112
|
+
onStack.delete(w);
|
|
113
|
+
component.push(w);
|
|
114
|
+
} while (w !== v);
|
|
115
|
+
sccs.push(component);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
for (const key of keyToTypename.keys()) {
|
|
119
|
+
if (!dfsIndex.has(key))
|
|
120
|
+
strongconnect(key);
|
|
121
|
+
}
|
|
122
|
+
// Tarjan emits SCCs in reverse topological order of the condensation.
|
|
123
|
+
// In our edge convention (child→parent), reverse-topo of the
|
|
124
|
+
// condensation means root-SCCs (no outgoing edges = no parents)
|
|
125
|
+
// first, leaf-SCCs (deepest descendants) last. We could just use
|
|
126
|
+
// emit-order as the priority — but that gives independent sibling
|
|
127
|
+
// SCCs different priorities, which is semantically wrong: siblings
|
|
128
|
+
// don't depend on each other and shouldn't be ordered relative to
|
|
129
|
+
// each other.
|
|
130
|
+
//
|
|
131
|
+
// Instead, do one more pass to compute *longest-path depth* on the
|
|
132
|
+
// condensation DAG: depth(SCC) = max(depth(parent SCC)) + 1, or 0
|
|
133
|
+
// for SCCs with no in-schema parents. SCCs at the same depth get
|
|
134
|
+
// the same priority — siblings stay tied, insertion order in the
|
|
135
|
+
// queue breaks the tie. Priority = (depth + 1) * 10.
|
|
136
|
+
//
|
|
137
|
+
// We can compute this in a single pass over the SCCs because
|
|
138
|
+
// Tarjan's emit-order *is* a valid topological order of the
|
|
139
|
+
// condensation: when we process sccs[i], every parent SCC has
|
|
140
|
+
// already been assigned a depth.
|
|
141
|
+
const nodeToSccIdx = new Map();
|
|
142
|
+
sccs.forEach((scc, i) => {
|
|
143
|
+
for (const node of scc)
|
|
144
|
+
nodeToSccIdx.set(node, i);
|
|
145
|
+
});
|
|
146
|
+
const sccDepth = new Map();
|
|
147
|
+
sccs.forEach((scc, i) => {
|
|
148
|
+
let maxParentDepth = -1;
|
|
149
|
+
for (const node of scc) {
|
|
150
|
+
for (const parent of parentsOf.get(node) ?? []) {
|
|
151
|
+
const parentSccIdx = nodeToSccIdx.get(parent);
|
|
152
|
+
if (parentSccIdx === undefined)
|
|
153
|
+
continue;
|
|
154
|
+
if (parentSccIdx === i)
|
|
155
|
+
continue; // intra-SCC edge — not a dep
|
|
156
|
+
const d = sccDepth.get(parentSccIdx);
|
|
157
|
+
if (d !== undefined && d > maxParentDepth)
|
|
158
|
+
maxParentDepth = d;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
sccDepth.set(i, maxParentDepth + 1);
|
|
162
|
+
});
|
|
163
|
+
const out = new Map();
|
|
164
|
+
sccs.forEach((scc, i) => {
|
|
165
|
+
const priority = (sccDepth.get(i) + 1) * 10;
|
|
166
|
+
for (const key of scc) {
|
|
167
|
+
out.set(keyToTypename.get(key), priority);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
export function deriveConfigFromSchema(schema) {
|
|
173
|
+
// Commit payload projection is done directly inside `TransactionQueue`
|
|
174
|
+
// — see `projectCommitPayload` there. Each model's field metadata
|
|
175
|
+
// rides on `ModelRegistry` (populated by `registerModelsFromSchema`),
|
|
176
|
+
// so there's no config-layer shim: the queue asks the registry for
|
|
177
|
+
// the declared fields and serializes accordingly.
|
|
178
|
+
return {
|
|
179
|
+
modelCreatePriority: computeFKDepthPriority(schema),
|
|
180
|
+
defaultCreatePriority: 40,
|
|
181
|
+
defaultNonCreatePriority: 50,
|
|
182
|
+
essentialFields: {},
|
|
183
|
+
classNameFallbackMap: {},
|
|
184
|
+
// Hash this client's schema once so bootstrap can detect drift against the
|
|
185
|
+
// server's active hash (same `schemaHash` the CLI push + server compute).
|
|
186
|
+
expectedSchemaHash: schemaHash(schema),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SchemaRecord } from '../schema/schema.js';
|
|
2
|
-
import type { AbloSession, CreateSessionParams } from './
|
|
2
|
+
import type { AbloSession, CreateSessionParams } from './resourceTypes.js';
|
|
3
3
|
/** The resolved control-plane context a mint needs. `fetch` is optional — the
|
|
4
4
|
* auth helpers fall back to the runtime global when omitted. */
|
|
5
5
|
export interface MintSessionContext {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* because the error messages reference URLs and would mislead if a
|
|
11
11
|
* URL was actually present.
|
|
12
12
|
*/
|
|
13
|
-
import { AbloValidationError } from '../errors.js';
|
|
13
|
+
import { AbloError, AbloValidationError } from '../errors.js';
|
|
14
14
|
export function validateAbloOptions(input) {
|
|
15
15
|
const { options, url, configuredApiKey, configuredAuthToken } = input;
|
|
16
16
|
const kind = options.kind ?? 'user';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The default `MutationExecutor` — sends `{ type: 'commit', ... }` frames over
|
|
3
|
+
* the engine's own WebSocket, resolved lazily at commit time (the WS doesn't
|
|
4
|
+
* exist when the executor is constructed).
|
|
5
|
+
*
|
|
6
|
+
* Extracted from `Ablo.ts`; the factory wires it up with a `getWs` closure
|
|
7
|
+
* over the store holder unless the caller supplies its own executor.
|
|
8
|
+
*/
|
|
9
|
+
import type { StaleNotification, ReadDependency } from '../coordination/schema.js';
|
|
10
|
+
import type { MutationExecutor, MutationOperation } from '../interfaces/index.js';
|
|
11
|
+
/**
|
|
12
|
+
* Default mutation executor: sends `{ type: 'commit', payload: ... }` over
|
|
13
|
+
* the sync engine's own WebSocket.
|
|
14
|
+
*
|
|
15
|
+
* Transport ownership follows the Zero / Liveblocks pattern — the engine
|
|
16
|
+
* owns its socket end-to-end and the executor is internal. Apps pass URLs
|
|
17
|
+
* and auth; they do NOT inject transport callbacks. That's why this
|
|
18
|
+
* factory takes a `getWs` closure instead of a full SyncWebSocket: the WS
|
|
19
|
+
* doesn't exist when the executor is constructed (it's created later in
|
|
20
|
+
* `Ablo` during `BaseSyncedStore` init), so we resolve it
|
|
21
|
+
* lazily at commit time. Same trick Zero uses internally — see
|
|
22
|
+
* `packages/zero-client/src/client/zero.ts` where `Pusher`/`Puller` are
|
|
23
|
+
* constructed before the socket then wired up at connect time.
|
|
24
|
+
*
|
|
25
|
+
* `options.idempotencyKey` becomes the wire-level `clientTxId` when set,
|
|
26
|
+
* matching Stripe-style retry semantics. Otherwise the SDK generates one.
|
|
27
|
+
*/
|
|
28
|
+
export declare function createDefaultMutationExecutor(getWs: () => {
|
|
29
|
+
sendCommit?: (operations: readonly MutationOperation[], clientTxId: string, timeoutMs?: number, causedByTaskId?: string | null, reads?: readonly ReadDependency[] | null) => Promise<{
|
|
30
|
+
lastSyncId: number;
|
|
31
|
+
notifications?: StaleNotification[];
|
|
32
|
+
}>;
|
|
33
|
+
} | null): MutationExecutor;
|