@abloatai/ablo 0.32.0 → 0.34.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 +35 -0
- package/README.md +24 -12
- package/dist/cli.cjs +148 -21
- package/dist/client/Ablo.js +3 -2
- package/dist/client/createModelProxy.d.ts +53 -10
- package/dist/client/createModelProxy.js +24 -5
- package/dist/client/httpTransport.js +1 -0
- package/dist/client/resourceTypes.d.ts +10 -1
- package/dist/client/wsMutationExecutor.d.ts +2 -2
- package/dist/client/wsMutationExecutor.js +1 -1
- package/dist/coordination/index.d.ts +2 -2
- package/dist/coordination/index.js +1 -1
- package/dist/coordination/schema.d.ts +20 -0
- package/dist/coordination/schema.js +22 -0
- package/dist/errorCodes.d.ts +1 -1
- package/dist/errorCodes.js +1 -1
- package/dist/interfaces/index.d.ts +13 -1
- package/dist/react/AbloProvider.d.ts +8 -8
- package/dist/react/AbloProvider.js +6 -6
- package/dist/react/index.d.ts +2 -2
- package/dist/react/index.js +2 -2
- package/dist/schema/index.d.ts +1 -1
- package/dist/schema/index.js +1 -1
- package/dist/schema/schema.d.ts +37 -40
- package/dist/schema/schema.js +24 -30
- package/dist/schema/select.js +1 -1
- package/dist/schema/serialize.d.ts +3 -3
- package/dist/schema/serialize.js +4 -2
- package/dist/server/commit.d.ts +8 -1
- package/dist/surface.d.ts +1 -1
- package/dist/surface.js +2 -1
- package/dist/sync/SyncWebSocket.d.ts +3 -3
- package/dist/sync/SyncWebSocket.js +4 -4
- package/dist/sync/commitFrames.d.ts +2 -2
- package/dist/sync/commitFrames.js +5 -1
- package/dist/transactions/TransactionQueue.d.ts +4 -1
- package/dist/transactions/TransactionQueue.js +18 -0
- package/dist/transactions/commitEnvelope.d.ts +8 -0
- package/dist/transactions/commitEnvelope.js +2 -1
- package/dist/transactions/durableWriteStore.d.ts +8 -0
- package/dist/wire/frames.d.ts +17 -1
- package/dist/wire/frames.js +2 -1
- package/docs/client-behavior.md +1 -1
- package/docs/coordination.md +18 -6
- package/docs/groups.md +50 -0
- package/docs/identity.md +2 -2
- package/docs/migration.md +29 -5
- package/docs/react.md +9 -9
- package/llms.txt +1 -1
- package/package.json +1 -1
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Exports are listed by name rather than re-exported wholesale, so every symbol
|
|
10
10
|
* that becomes part of this package's public API is a deliberate choice.
|
|
11
11
|
*/
|
|
12
|
-
export { targetRangeSchema, participantKindSchema, wireParticipantKindSchema, participantKindFromWire, descriptionFromMeta, targetRefSchema, onStaleModeSchema, writeGuardSchema, staleNotificationSchema, readDependencySchema, claimStatusSchema, wireClaimSummarySchema, claimErrorSchema, wireClaimSchema, claimRejectionSchema, modelTargetSchema, modelClaimSchema, claimBeginPayloadSchema, claimAbandonPayloadSchema, claimReorderPayloadSchema, claimHeartbeatPayloadSchema, claimHeartbeatAckPayloadSchema, claimHeartbeatBatchPayloadSchema, claimHeartbeatBatchAckPayloadSchema, updateSubscriptionPayloadSchema, subscriptionAckPayloadSchema, commitOperationTypeSchema, commitOperationSchema, presenceKindSchema, presenceActivitySchema, presenceUpdateFrameSchema, } from './schema.js';
|
|
12
|
+
export { targetRangeSchema, participantKindSchema, wireParticipantKindSchema, participantKindFromWire, descriptionFromMeta, targetRefSchema, onStaleModeSchema, writeGuardSchema, staleNotificationSchema, readDependencySchema, trackDependencySchema, claimStatusSchema, wireClaimSummarySchema, claimErrorSchema, wireClaimSchema, claimRejectionSchema, modelTargetSchema, modelClaimSchema, claimBeginPayloadSchema, claimAbandonPayloadSchema, claimReorderPayloadSchema, claimHeartbeatPayloadSchema, claimHeartbeatAckPayloadSchema, claimHeartbeatBatchPayloadSchema, claimHeartbeatBatchAckPayloadSchema, updateSubscriptionPayloadSchema, subscriptionAckPayloadSchema, commitOperationTypeSchema, commitOperationSchema, presenceKindSchema, presenceActivitySchema, presenceUpdateFrameSchema, } from './schema.js';
|
|
13
13
|
export { defaultPolicy, capabilityPreemptPolicy, interpretConflictAxis } from '../policy/types.js';
|
|
14
14
|
export type { Conflict, ConflictAxis, ConflictDecision, ConflictKind, ConflictOperation, ConflictPolicy, StaleContextConflict, ClaimHeldConflict, } from '../policy/types.js';
|
|
15
|
-
export type { TargetRange, ParticipantKind, TargetRef, OnStaleMode, WriteGuard, StaleNotification, ReadDependency, ClaimStatus, WireClaimSummary, ClaimError, WireClaim, ClaimRejection, ModelTarget, ModelClaim, ClaimBeginPayload, ClaimAbandonPayload, ClaimReorderPayload, ClaimHeartbeatPayload, ClaimHeartbeatAckPayload, ClaimHeartbeatBatchPayload, ClaimHeartbeatBatchAckPayload, UpdateSubscriptionPayload, SubscriptionAckPayload, CommitOperationType, CommitOperation, AnyCommitOperation, PresenceKind, PresenceActivity, PresenceUpdateFrame, } from './schema.js';
|
|
15
|
+
export type { TargetRange, ParticipantKind, TargetRef, OnStaleMode, WriteGuard, StaleNotification, ReadDependency, TrackDependency, ClaimStatus, WireClaimSummary, ClaimError, WireClaim, ClaimRejection, ModelTarget, ModelClaim, ClaimBeginPayload, ClaimAbandonPayload, ClaimReorderPayload, ClaimHeartbeatPayload, ClaimHeartbeatAckPayload, ClaimHeartbeatBatchPayload, ClaimHeartbeatBatchAckPayload, UpdateSubscriptionPayload, SubscriptionAckPayload, CommitOperationType, CommitOperation, AnyCommitOperation, PresenceKind, PresenceActivity, PresenceUpdateFrame, } from './schema.js';
|
|
@@ -14,7 +14,7 @@ export {
|
|
|
14
14
|
// Shared primitives
|
|
15
15
|
targetRangeSchema, participantKindSchema, wireParticipantKindSchema, participantKindFromWire, descriptionFromMeta, targetRefSchema,
|
|
16
16
|
// Layer 3 — optimistic stale-context
|
|
17
|
-
onStaleModeSchema, writeGuardSchema, staleNotificationSchema, readDependencySchema,
|
|
17
|
+
onStaleModeSchema, writeGuardSchema, staleNotificationSchema, readDependencySchema, trackDependencySchema,
|
|
18
18
|
// Layer 2 — pessimistic claim / claim-lease
|
|
19
19
|
claimStatusSchema, wireClaimSummarySchema, claimErrorSchema, wireClaimSchema, claimRejectionSchema, modelTargetSchema, modelClaimSchema, claimBeginPayloadSchema, claimAbandonPayloadSchema, claimReorderPayloadSchema, claimHeartbeatPayloadSchema, claimHeartbeatAckPayloadSchema, claimHeartbeatBatchPayloadSchema, claimHeartbeatBatchAckPayloadSchema,
|
|
20
20
|
// Read interest — area-of-interest navigation
|
|
@@ -181,6 +181,26 @@ export declare const readDependencySchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
|
181
181
|
}>>;
|
|
182
182
|
}, z.core.$strip>]>;
|
|
183
183
|
export type ReadDependency = z.infer<typeof readDependencySchema>;
|
|
184
|
+
/**
|
|
185
|
+
* A durable read-dependency — what a participant is watching so that a later
|
|
186
|
+
* change to it opens a {@link StaleNotification}. It is the persisted sibling of
|
|
187
|
+
* a {@link ReadDependency}: the same reference shape, minus the disposition (a
|
|
188
|
+
* track always notifies — that is what tracking is), with an optional `readAt`
|
|
189
|
+
* that defaults to the watermark of the commit that registered it. The row form
|
|
190
|
+
* watches one object; the group form watches a whole sync group ("anything in
|
|
191
|
+
* `deck:abc`"). Where a `ReadDependency` is checked once at commit and discarded,
|
|
192
|
+
* a `TrackDependency` is kept and re-checked against every future delta. See
|
|
193
|
+
* `packages/sync-engine/docs/groups.md` for how it drives change propagation.
|
|
194
|
+
*/
|
|
195
|
+
export declare const trackDependencySchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
196
|
+
model: z.ZodString;
|
|
197
|
+
id: z.ZodString;
|
|
198
|
+
readAt: z.ZodOptional<z.ZodNumber>;
|
|
199
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
200
|
+
group: z.ZodString;
|
|
201
|
+
readAt: z.ZodOptional<z.ZodNumber>;
|
|
202
|
+
}, z.core.$strip>]>;
|
|
203
|
+
export type TrackDependency = z.infer<typeof trackDependencySchema>;
|
|
184
204
|
/**
|
|
185
205
|
* The lifecycle of a claim. When absent on the wire it means `'active'` (an
|
|
186
206
|
* additive back-compat default). The server stamps `'active'` on `claim_begin`
|
|
@@ -183,6 +183,28 @@ export const readDependencySchema = z.union([
|
|
|
183
183
|
onStale: onStaleModeSchema.optional(),
|
|
184
184
|
}),
|
|
185
185
|
]);
|
|
186
|
+
/**
|
|
187
|
+
* A durable read-dependency — what a participant is watching so that a later
|
|
188
|
+
* change to it opens a {@link StaleNotification}. It is the persisted sibling of
|
|
189
|
+
* a {@link ReadDependency}: the same reference shape, minus the disposition (a
|
|
190
|
+
* track always notifies — that is what tracking is), with an optional `readAt`
|
|
191
|
+
* that defaults to the watermark of the commit that registered it. The row form
|
|
192
|
+
* watches one object; the group form watches a whole sync group ("anything in
|
|
193
|
+
* `deck:abc`"). Where a `ReadDependency` is checked once at commit and discarded,
|
|
194
|
+
* a `TrackDependency` is kept and re-checked against every future delta. See
|
|
195
|
+
* `packages/sync-engine/docs/groups.md` for how it drives change propagation.
|
|
196
|
+
*/
|
|
197
|
+
export const trackDependencySchema = z.union([
|
|
198
|
+
z.object({
|
|
199
|
+
model: z.string(),
|
|
200
|
+
id: z.string(),
|
|
201
|
+
readAt: z.number().optional(),
|
|
202
|
+
}),
|
|
203
|
+
z.object({
|
|
204
|
+
group: z.string(),
|
|
205
|
+
readAt: z.number().optional(),
|
|
206
|
+
}),
|
|
207
|
+
]);
|
|
186
208
|
// ─────────────────────────────────────────────────────────────────────────
|
|
187
209
|
// Layer 2 — pessimistic claims and leases
|
|
188
210
|
// ─────────────────────────────────────────────────────────────────────────
|
package/dist/errorCodes.d.ts
CHANGED
|
@@ -161,7 +161,7 @@ export declare const ERROR_CODES: {
|
|
|
161
161
|
readonly model_claimed: ErrorCodeSpec;
|
|
162
162
|
readonly model_claimed_timeout: ErrorCodeSpec;
|
|
163
163
|
readonly model_claim_not_configured: ErrorCodeSpec;
|
|
164
|
-
readonly
|
|
164
|
+
readonly model_join_not_configured: ErrorCodeSpec;
|
|
165
165
|
readonly stale_context: ErrorCodeSpec;
|
|
166
166
|
readonly contention_exhausted: ErrorCodeSpec;
|
|
167
167
|
readonly update_aborted: ErrorCodeSpec;
|
package/dist/errorCodes.js
CHANGED
|
@@ -157,7 +157,7 @@ export const ERROR_CODES = {
|
|
|
157
157
|
model_claimed: wire('claim', 409, false, 'Another participant holds a claim on this row. Read `claim.state` to see who holds it, or queue behind them with a claim of your own.'),
|
|
158
158
|
model_claimed_timeout: wire('claim', 409, false, 'Another participant held a claim on this row and did not release it in time. Retry, or read `claim.state` to see who holds it.'),
|
|
159
159
|
model_claim_not_configured: client('claim', 'Claiming requires the collaboration runtime, which the standard Ablo({ schema, apiKey }) client wires up for every model automatically — there is no per-model claim configuration to add. This appears only when a model proxy is constructed directly without that runtime (an internal/advanced path).'),
|
|
160
|
-
|
|
160
|
+
model_join_not_configured: client('claim', 'join() opens a presence/claim subscription and needs a live WebSocket, so it is unavailable on the HTTP transport and on model proxies built without a socket. Use the standard Ablo({ schema, apiKey }) client (default WebSocket transport).'),
|
|
161
161
|
// ── stale context / idempotency (409) ──────────────────────────────
|
|
162
162
|
// Not retryable at the transport: the rejected request carries its frozen
|
|
163
163
|
// `readAt`, so resending the identical payload can never succeed. Recovery
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* session-error detection, online-status checks, and the transport that carries
|
|
7
7
|
* mutations to your backend. The SDK ships sensible no-op defaults where it can.
|
|
8
8
|
*/
|
|
9
|
-
import type { ReadDependency, ParticipantKind } from '../coordination/schema.js';
|
|
9
|
+
import type { ReadDependency, TrackDependency, ParticipantKind } from '../coordination/schema.js';
|
|
10
10
|
import type { CommitStatus, MutationCommitResultInput } from '../wire/commit.js';
|
|
11
11
|
export interface SyncLogger {
|
|
12
12
|
debug(message: string, ...args: unknown[]): void;
|
|
@@ -232,6 +232,18 @@ export interface MutationOptions {
|
|
|
232
232
|
* footprints, §4 the read-set) for the governing convention.
|
|
233
233
|
*/
|
|
234
234
|
reads?: ReadDependency[] | null;
|
|
235
|
+
/**
|
|
236
|
+
* Durable read-dependencies — what this write (or the record it produces) should
|
|
237
|
+
* keep watching. Unlike `reads`, which is checked once at commit and discarded,
|
|
238
|
+
* each `track` entry is persisted and re-checked against every future delta; a
|
|
239
|
+
* later matching change opens a `StaleNotification` for the tracking participant,
|
|
240
|
+
* delivered at their next commit or live to a held claim. Each entry is a row
|
|
241
|
+
* (`{ model, id }`) or a sync group (`{ group }`), optionally pinned to a `readAt`
|
|
242
|
+
* baseline (defaults to this commit's watermark).
|
|
243
|
+
*
|
|
244
|
+
* See `packages/sync-engine/docs/groups.md` for how `track` drives propagation.
|
|
245
|
+
*/
|
|
246
|
+
track?: TrackDependency[] | null;
|
|
235
247
|
}
|
|
236
248
|
/**
|
|
237
249
|
* The subset of {@link MutationOptions} that travels with each write as it is
|
|
@@ -15,7 +15,7 @@ import { type SyncStoreContract } from './context.js';
|
|
|
15
15
|
* - **One component, one import.** Consumers write the provider
|
|
16
16
|
* once at the root; nothing else needs to plumb the engine.
|
|
17
17
|
* - **Multiplayer is default.** React consumers are always browsers doing
|
|
18
|
-
* multiplayer UI, so `
|
|
18
|
+
* multiplayer UI, so `useJoin()` / `useAblo()` are always
|
|
19
19
|
* available. No opt-in prop.
|
|
20
20
|
* - **Declarative props for app glue.** `preventUnsavedChanges`,
|
|
21
21
|
* `onSessionExpired`, `postBootstrap`, `resolveUsers` — each
|
|
@@ -110,11 +110,11 @@ export interface AbloProviderProps<R extends SchemaRecord = SchemaRecord> {
|
|
|
110
110
|
export declare function AbloProvider<R extends SchemaRecord = SchemaRecord>(props: AbloProviderProps<R>): React.ReactElement;
|
|
111
111
|
export type { EngineParticipant, ParticipantScope, ParticipantStatus };
|
|
112
112
|
/**
|
|
113
|
-
* Options for `
|
|
113
|
+
* Options for `useJoin`. The hook reuses the engine's single
|
|
114
114
|
* WebSocket and opens a scoped claim on it when `scope` is provided:
|
|
115
115
|
* one TCP connection, N logical sub-syncgroup participants.
|
|
116
116
|
*/
|
|
117
|
-
export interface
|
|
117
|
+
export interface UseJoinOptions {
|
|
118
118
|
readonly scope?: ParticipantScope;
|
|
119
119
|
readonly ttlSeconds?: number | string | null;
|
|
120
120
|
/** Tear down + don't re-join while true. */
|
|
@@ -145,7 +145,7 @@ export interface UseWatchOptions {
|
|
|
145
145
|
}
|
|
146
146
|
/** @deprecated Use `ParticipantStatus`. */
|
|
147
147
|
export type MeshParticipantStatus = ParticipantStatus;
|
|
148
|
-
export interface
|
|
148
|
+
export interface UseJoinReturn {
|
|
149
149
|
readonly participant: EngineParticipant | null;
|
|
150
150
|
/** Everyone else on the engine's sync groups (`participant.presence.others`), bridged to React. */
|
|
151
151
|
readonly peers: readonly Peer[];
|
|
@@ -159,7 +159,7 @@ export interface UseWatchReturn {
|
|
|
159
159
|
* lifecycle status. Auto-cleans up on unmount or when `paused`
|
|
160
160
|
* flips to true.
|
|
161
161
|
*
|
|
162
|
-
* `
|
|
162
|
+
* `useJoin` is the React form of `ablo.<model>.join` — scope-level
|
|
163
163
|
* read-interest + presence; returns the reactive participant facade
|
|
164
164
|
* (peers/claims/status).
|
|
165
165
|
*
|
|
@@ -168,10 +168,10 @@ export interface UseWatchReturn {
|
|
|
168
168
|
* headless-bot patterns (a separate identity in the same browser
|
|
169
169
|
* tab), construct a second `Ablo({ kind: 'agent', ... })` directly.
|
|
170
170
|
*/
|
|
171
|
-
export declare function
|
|
171
|
+
export declare function useJoin(opts: UseJoinOptions): UseJoinReturn;
|
|
172
172
|
/**
|
|
173
173
|
* Read-only presence: the OTHER participants currently visible to this
|
|
174
|
-
* connection, bridged to React. Unlike {@link
|
|
174
|
+
* connection, bridged to React. Unlike {@link useJoin}, this does
|
|
175
175
|
* NOT enter/leave a scope (no `update_subscription`, no warm-TTL churn) —
|
|
176
176
|
* it is a pure reader of the engine's already-flowing presence stream.
|
|
177
177
|
*
|
|
@@ -184,7 +184,7 @@ export declare function useWatch(opts: UseWatchOptions): UseWatchReturn;
|
|
|
184
184
|
* Use this to answer "is anyone else here?" — e.g. suppressing live-cursor
|
|
185
185
|
* broadcasts while alone — when some OTHER mount already owns the scope's
|
|
186
186
|
* read interest (scope `leave` is not reference-counted, so a second
|
|
187
|
-
* `
|
|
187
|
+
* `useJoin` on the same scope would warm-drop the owner's
|
|
188
188
|
* subscription on unmount).
|
|
189
189
|
*
|
|
190
190
|
* ```ts
|
|
@@ -211,7 +211,7 @@ const EMPTY_INTENTS = Object.freeze([]);
|
|
|
211
211
|
* lifecycle status. Auto-cleans up on unmount or when `paused`
|
|
212
212
|
* flips to true.
|
|
213
213
|
*
|
|
214
|
-
* `
|
|
214
|
+
* `useJoin` is the React form of `ablo.<model>.join` — scope-level
|
|
215
215
|
* read-interest + presence; returns the reactive participant facade
|
|
216
216
|
* (peers/claims/status).
|
|
217
217
|
*
|
|
@@ -220,7 +220,7 @@ const EMPTY_INTENTS = Object.freeze([]);
|
|
|
220
220
|
* headless-bot patterns (a separate identity in the same browser
|
|
221
221
|
* tab), construct a second `Ablo({ kind: 'agent', ... })` directly.
|
|
222
222
|
*/
|
|
223
|
-
export function
|
|
223
|
+
export function useJoin(opts) {
|
|
224
224
|
const ctx = useContext(AbloInternalContext);
|
|
225
225
|
const engine = ctx?.engine ?? null;
|
|
226
226
|
const { paused = false } = opts;
|
|
@@ -353,7 +353,7 @@ export function useWatch(opts) {
|
|
|
353
353
|
}
|
|
354
354
|
/**
|
|
355
355
|
* Read-only presence: the OTHER participants currently visible to this
|
|
356
|
-
* connection, bridged to React. Unlike {@link
|
|
356
|
+
* connection, bridged to React. Unlike {@link useJoin}, this does
|
|
357
357
|
* NOT enter/leave a scope (no `update_subscription`, no warm-TTL churn) —
|
|
358
358
|
* it is a pure reader of the engine's already-flowing presence stream.
|
|
359
359
|
*
|
|
@@ -366,7 +366,7 @@ export function useWatch(opts) {
|
|
|
366
366
|
* Use this to answer "is anyone else here?" — e.g. suppressing live-cursor
|
|
367
367
|
* broadcasts while alone — when some OTHER mount already owns the scope's
|
|
368
368
|
* read interest (scope `leave` is not reference-counted, so a second
|
|
369
|
-
* `
|
|
369
|
+
* `useJoin` on the same scope would warm-drop the owner's
|
|
370
370
|
* subscription on unmount).
|
|
371
371
|
*
|
|
372
372
|
* ```ts
|
|
@@ -377,7 +377,7 @@ export function useWatch(opts) {
|
|
|
377
377
|
export function usePeers(scope) {
|
|
378
378
|
const ctx = useContext(AbloInternalContext);
|
|
379
379
|
const engine = ctx?.engine ?? null;
|
|
380
|
-
// Resolve scope → groups through the schema (same idiom as
|
|
380
|
+
// Resolve scope → groups through the schema (same idiom as useJoin).
|
|
381
381
|
// The stringified, sorted key is the stable effect dependency.
|
|
382
382
|
const scopeKey = JSON.stringify(resolveParticipantSyncGroups(scope, engine?.schema).sort());
|
|
383
383
|
const groups = useMemo(() => JSON.parse(scopeKey), [scopeKey]);
|
|
@@ -394,7 +394,7 @@ export function usePeers(scope) {
|
|
|
394
394
|
// Plain useState + onChange — presence changes on join/leave/activity
|
|
395
395
|
// only (never on cursor traffic, a separate channel), so this fires
|
|
396
396
|
// rarely; a frame of stale presence is harmless (same rationale as
|
|
397
|
-
//
|
|
397
|
+
// useJoin's peers bridge).
|
|
398
398
|
setPeers(compute());
|
|
399
399
|
return presence.onChange(() => { setPeers(compute()); });
|
|
400
400
|
}, [engine, scopeKey]);
|
package/dist/react/index.d.ts
CHANGED
|
@@ -41,10 +41,10 @@
|
|
|
41
41
|
* client:
|
|
42
42
|
*
|
|
43
43
|
* useAblo((ablo) => ablo.<model>.claim.state(...)) — reactive coordination reads
|
|
44
|
-
*
|
|
44
|
+
* useJoin({ scope }) — join a scope to get its peers and claims
|
|
45
45
|
*/
|
|
46
46
|
export type { DefaultSyncShape, ResolveSchema, ResolveUserMeta, ResolveModelKey, } from '../types/global.js';
|
|
47
|
-
export { AbloProvider,
|
|
47
|
+
export { AbloProvider, useJoin, usePeers, useSync, useSyncStore, type AbloProviderProps, type ParticipantScope, type ParticipantStatus, type UseJoinOptions, type UseJoinReturn, type MeshParticipantStatus, } from './AbloProvider.js';
|
|
48
48
|
export { ClientSideSuspense, type ClientSideSuspenseProps, } from './ClientSideSuspense.js';
|
|
49
49
|
export { DefaultFallback } from './DefaultFallback.js';
|
|
50
50
|
export type { SyncStoreContract } from './context.js';
|
package/dist/react/index.js
CHANGED
|
@@ -41,10 +41,10 @@
|
|
|
41
41
|
* client:
|
|
42
42
|
*
|
|
43
43
|
* useAblo((ablo) => ablo.<model>.claim.state(...)) — reactive coordination reads
|
|
44
|
-
*
|
|
44
|
+
* useJoin({ scope }) — join a scope to get its peers and claims
|
|
45
45
|
*/
|
|
46
46
|
// ── Umbrella provider + lifecycle hooks ────────────────────────────
|
|
47
|
-
export { AbloProvider,
|
|
47
|
+
export { AbloProvider, useJoin, usePeers, useSync, useSyncStore, } from './AbloProvider.js';
|
|
48
48
|
export { ClientSideSuspense, } from './ClientSideSuspense.js';
|
|
49
49
|
export { DefaultFallback } from './DefaultFallback.js';
|
|
50
50
|
// ── Status + errors + identity ─────────────────────────────────────
|
package/dist/schema/index.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ export { syncDeltaActionSchema, wireDeltaDataSchema, participantRefSchema, syncD
|
|
|
30
30
|
export { model, scopeKindOf, type ModelDef, type ModelOptions, type LoadStrategy, type PersistOptions, type RelationRecord, type GrantsRef, type ConflictAxis, } from './model.js';
|
|
31
31
|
export { coordination, humansOverwrite, humansReject, humansNotify, agentsOverwrite, agentsReject, agentsNotify, systemOverwrite, systemReject, systemNotify, type ConflictRule, } from './coordination.js';
|
|
32
32
|
export { mutable, readOnly, type SugarOptions } from './sugar.js';
|
|
33
|
-
export { defineSchema, composeIdentitySyncGroups, type Schema, type SchemaRecord, type Model, type Row, type InferModel, type InferCreate, type InferRow, type InferModelNames, type BaseModelFields, type InsertValue, type UpsertValue, type UpdateValue, type DeleteId, type DefineSchemaOptions, type Casing, type CasingConvention, type CasingFn, composeEntitySyncGroups, intersectRequestedWithAllowed, type IdentityRole, type IdentityContext, type IdentityRoleSource, type EntityRole, type EntityContext, type EntityRoleSource, type RoleSource, type RoleContext, type SyncGroup, type SyncGroupInput, identityRole, entityRole, extractIdentityIds, extractEntityIds, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, groupsInputSchema, type GroupsInput, type
|
|
33
|
+
export { defineSchema, composeIdentitySyncGroups, type Schema, type SchemaRecord, type Model, type Row, type InferModel, type InferCreate, type InferRow, type InferModelNames, type BaseModelFields, type InsertValue, type UpsertValue, type UpdateValue, type DeleteId, type DefineSchemaOptions, type Casing, type CasingConvention, type CasingFn, composeEntitySyncGroups, intersectRequestedWithAllowed, type IdentityRole, type IdentityContext, type IdentityRoleSource, type EntityRole, type EntityContext, type EntityRoleSource, type RoleSource, type RoleContext, type SyncGroup, type SyncGroupInput, identityRole, entityRole, extractIdentityIds, extractEntityIds, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, groupsInputSchema, type GroupsInput, type SessionSettings, type SessionSettingSource, RESERVED_SESSION_SETTINGS, } from './schema.js';
|
|
34
34
|
export { serializeSchema, parseSchema, toSchemaJSON, fromSchemaJSON, schemaHash, type SchemaJSON, type ModelJSON, type RelationJSON, } from './serialize.js';
|
|
35
35
|
export { selectModels, omitModels } from './select.js';
|
|
36
36
|
export { generateProvisionPlan, generateMigrationPlan, appSchemaName, camelToSnake, snakeToCamel, q, sqlType, type ProvisionPlan, type MigrationPlan, } from './ddl.js';
|
package/dist/schema/index.js
CHANGED
|
@@ -56,7 +56,7 @@ export { coordination, humansOverwrite, humansReject, humansNotify, agentsOverwr
|
|
|
56
56
|
// sensible defaults for everything else.
|
|
57
57
|
export { mutable, readOnly } from './sugar.js';
|
|
58
58
|
// Schema definition + type inference
|
|
59
|
-
export { defineSchema, composeIdentitySyncGroups, composeEntitySyncGroups, intersectRequestedWithAllowed, identityRole, entityRole, extractIdentityIds, extractEntityIds, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, groupsInputSchema,
|
|
59
|
+
export { defineSchema, composeIdentitySyncGroups, composeEntitySyncGroups, intersectRequestedWithAllowed, identityRole, entityRole, extractIdentityIds, extractEntityIds, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, groupsInputSchema, RESERVED_SESSION_SETTINGS, } from './schema.js';
|
|
60
60
|
// Schema ⇄ JSON — serialize a schema for transport and rebuild it on the far side.
|
|
61
61
|
export { serializeSchema, parseSchema, toSchemaJSON, fromSchemaJSON, schemaHash, } from './serialize.js';
|
|
62
62
|
// Schema projection — derive an app's subset from one canonical schema.
|
package/dist/schema/schema.d.ts
CHANGED
|
@@ -30,36 +30,32 @@ export type CasingConvention = 'snake_case' | 'camelCase';
|
|
|
30
30
|
export type CasingFn = (jsField: string) => string;
|
|
31
31
|
/** `defineSchema`'s casing option. Identity when unset. */
|
|
32
32
|
export type Casing = CasingConvention | CasingFn;
|
|
33
|
-
/** Options for `defineSchema`. */
|
|
34
33
|
/**
|
|
35
|
-
* A server-authored
|
|
36
|
-
*
|
|
37
|
-
* authenticated `ek_` and the plane, never from client-supplied data —
|
|
38
|
-
* mapping can forward the tenant identity Ablo already trusts but can never
|
|
34
|
+
* A server-authored identity value that can fill one of a customer's Postgres
|
|
35
|
+
* session settings. Deliberately a closed set — every member is resolved by Ablo
|
|
36
|
+
* from the authenticated `ek_` and the plane, never from client-supplied data —
|
|
37
|
+
* so a mapping can forward the tenant identity Ablo already trusts but can never
|
|
39
38
|
* widen a writer's scope. Mirrors the fields the engine sets on the direct-write
|
|
40
39
|
* connection (`app.current_org_id`, `app.current_project_id`, …).
|
|
41
40
|
*/
|
|
42
|
-
export type
|
|
41
|
+
export type SessionSettingSource = 'orgId' | 'projectId' | 'environment' | 'sandboxId' | 'participantId' | 'participantKind';
|
|
43
42
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
43
|
+
* A map from a Postgres session-setting name the customer's RLS policies read to
|
|
44
|
+
* the Ablo identity that fills it — e.g. `{ 'app.current_org': 'orgId' }`. The
|
|
45
|
+
* setting name is the key (it takes exactly one source), so a duplicate is
|
|
46
|
+
* unrepresentable rather than validated away. See
|
|
47
|
+
* {@link DefineSchemaOptions.sessionSettings} and ADR 0011.
|
|
47
48
|
*/
|
|
48
|
-
export
|
|
49
|
-
/** The GUC name the customer's policies read, e.g. `app.current_app_org_id`. */
|
|
50
|
-
readonly guc: string;
|
|
51
|
-
/** Which server-authored context value fills it. */
|
|
52
|
-
readonly from: TenantContextSource;
|
|
53
|
-
}
|
|
49
|
+
export type SessionSettings = Readonly<Record<string, SessionSettingSource>>;
|
|
54
50
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* may never reassign one of these, which would
|
|
58
|
-
* engine's own scoping, timeouts, or `row_security`.
|
|
59
|
-
* direct-write seam so authoring-time validation and
|
|
60
|
-
* list. See ADR 0011.
|
|
51
|
+
* Session settings Ablo already sets on its direct-write connection. A
|
|
52
|
+
* `sessionSettings` entry FORWARDS Ablo's trusted context into a setting the
|
|
53
|
+
* customer's own policies read — it may never reassign one of these, which would
|
|
54
|
+
* let a schema push relax the engine's own scoping, timeouts, or `row_security`.
|
|
55
|
+
* Shared with the engine's direct-write seam so authoring-time validation and
|
|
56
|
+
* the runtime guard read one list. See ADR 0011.
|
|
61
57
|
*/
|
|
62
|
-
export declare const
|
|
58
|
+
export declare const RESERVED_SESSION_SETTINGS: readonly string[];
|
|
63
59
|
export interface DefineSchemaOptions {
|
|
64
60
|
/**
|
|
65
61
|
* How to translate camelCase JS field names into database column
|
|
@@ -90,26 +86,27 @@ export interface DefineSchemaOptions {
|
|
|
90
86
|
*/
|
|
91
87
|
readonly identityRoles?: readonly IdentityRole[];
|
|
92
88
|
/**
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
* write, Ablo already
|
|
96
|
-
* `app.current_project_id`, …)
|
|
97
|
-
* differently-named
|
|
98
|
-
* restrictive policy on `current_setting('app.
|
|
99
|
-
*
|
|
100
|
-
* your RLS governs Ablo's writes:
|
|
89
|
+
* Extra Postgres session settings Ablo's direct-write connection applies from
|
|
90
|
+
* your authenticated identity, so your row-level-security policies read them
|
|
91
|
+
* (ADR 0011). Before every direct write, Ablo already `SET LOCAL`s a fixed
|
|
92
|
+
* bundle (`app.current_org_id`, `app.current_project_id`, …). If your policies
|
|
93
|
+
* read a differently-named setting your own app sets per-connection — e.g. a
|
|
94
|
+
* restrictive policy on `current_setting('app.current_org')` — map that
|
|
95
|
+
* setting to the identity that fills it, and Ablo sets it too:
|
|
101
96
|
*
|
|
102
97
|
* ```ts
|
|
103
98
|
* defineSchema({ ... }, {
|
|
104
|
-
*
|
|
99
|
+
* sessionSettings: { 'app.current_org': 'orgId' },
|
|
105
100
|
* })
|
|
106
101
|
* ```
|
|
107
102
|
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
103
|
+
* The key is the setting name your policies read; the value is a closed set of
|
|
104
|
+
* identities Ablo authenticates, so a mapping can narrow what the writer sees
|
|
105
|
+
* but never widen it. Do NOT carve a policy exception for the writer role —
|
|
106
|
+
* that exempts exactly the writes you want governed. Leave unset when your
|
|
107
|
+
* policies read the settings Ablo already applies (or when a table has no RLS).
|
|
111
108
|
*/
|
|
112
|
-
readonly
|
|
109
|
+
readonly sessionSettings?: SessionSettings;
|
|
113
110
|
}
|
|
114
111
|
/** A record of model names → model definitions */
|
|
115
112
|
export type SchemaRecord = Record<string, ModelDef>;
|
|
@@ -165,12 +162,12 @@ export interface Schema<S extends SchemaRecord = SchemaRecord> {
|
|
|
165
162
|
*/
|
|
166
163
|
readonly identityRoles: readonly IdentityRole[];
|
|
167
164
|
/**
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
165
|
+
* Session settings registered via `defineSchema({...}, { sessionSettings })`
|
|
166
|
+
* (ADR 0011). The engine's direct-write path reads this to `SET LOCAL` each
|
|
167
|
+
* named setting from server-authored context before applying DML, so the
|
|
168
|
+
* customer's RLS governs Ablo's writes. Empty object when unset.
|
|
172
169
|
*/
|
|
173
|
-
readonly
|
|
170
|
+
readonly sessionSettings: SessionSettings;
|
|
174
171
|
/**
|
|
175
172
|
* Set only on a projection produced by `selectModels`/`omitModels`: the
|
|
176
173
|
* content hash of the FULL source schema the subset was cut from. A subset
|
package/dist/schema/schema.js
CHANGED
|
@@ -45,14 +45,14 @@ function camelToSnake(identifier) {
|
|
|
45
45
|
return identifier.replace(/[A-Z]/g, (ch) => `_${ch.toLowerCase()}`);
|
|
46
46
|
}
|
|
47
47
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* may never reassign one of these, which would
|
|
51
|
-
* engine's own scoping, timeouts, or `row_security`.
|
|
52
|
-
* direct-write seam so authoring-time validation and
|
|
53
|
-
* list. See ADR 0011.
|
|
48
|
+
* Session settings Ablo already sets on its direct-write connection. A
|
|
49
|
+
* `sessionSettings` entry FORWARDS Ablo's trusted context into a setting the
|
|
50
|
+
* customer's own policies read — it may never reassign one of these, which would
|
|
51
|
+
* let a schema push relax the engine's own scoping, timeouts, or `row_security`.
|
|
52
|
+
* Shared with the engine's direct-write seam so authoring-time validation and
|
|
53
|
+
* the runtime guard read one list. See ADR 0011.
|
|
54
54
|
*/
|
|
55
|
-
export const
|
|
55
|
+
export const RESERVED_SESSION_SETTINGS = [
|
|
56
56
|
'statement_timeout',
|
|
57
57
|
'lock_timeout',
|
|
58
58
|
'row_security',
|
|
@@ -221,45 +221,39 @@ export function defineSchema(models, options) {
|
|
|
221
221
|
resolvedModels[name] = { ...def, typename, tableName, persist };
|
|
222
222
|
}
|
|
223
223
|
validateSyncGroupSchema(resolvedModels);
|
|
224
|
-
|
|
224
|
+
validateSessionSettings(options?.sessionSettings ?? {});
|
|
225
225
|
return {
|
|
226
226
|
// Cast back to S: we only added values to optional fields that were
|
|
227
227
|
// already part of ModelDef, so the shape is structurally unchanged.
|
|
228
228
|
models: resolvedModels,
|
|
229
229
|
validators: validators,
|
|
230
230
|
identityRoles: options?.identityRoles ?? [],
|
|
231
|
-
|
|
231
|
+
sessionSettings: options?.sessionSettings ?? {},
|
|
232
232
|
};
|
|
233
233
|
}
|
|
234
234
|
/**
|
|
235
|
-
* Reject
|
|
235
|
+
* Reject session-setting mappings that couldn't do what the author intends —
|
|
236
236
|
* caught here at definition time rather than silently dropped on the write path.
|
|
237
|
-
*
|
|
238
|
-
*
|
|
237
|
+
* Each key must name a non-empty setting the customer's policies read, and must
|
|
238
|
+
* not reassign one Ablo already manages. Uniqueness needs no check: the map is
|
|
239
|
+
* keyed by the setting name, so a duplicate is unrepresentable.
|
|
239
240
|
*/
|
|
240
|
-
function
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
const name = guc.trim();
|
|
241
|
+
function validateSessionSettings(settings) {
|
|
242
|
+
for (const setting of Object.keys(settings)) {
|
|
243
|
+
const name = setting.trim();
|
|
244
244
|
if (name === '') {
|
|
245
|
-
throw new AbloValidationError(`[defineSchema]
|
|
246
|
-
`
|
|
247
|
-
`\`{
|
|
245
|
+
throw new AbloValidationError(`[defineSchema] sessionSettings: a key is empty. Name the Postgres ` +
|
|
246
|
+
`setting your row-level-security policies read, e.g. ` +
|
|
247
|
+
`\`{ 'app.current_org': 'orgId' }\`.`, { code: 'schema_definition_invalid', param: 'sessionSettings' });
|
|
248
248
|
}
|
|
249
|
-
if (
|
|
250
|
-
throw new AbloValidationError(`[defineSchema]
|
|
249
|
+
if (RESERVED_SESSION_SETTINGS.includes(name)) {
|
|
250
|
+
throw new AbloValidationError(`[defineSchema] sessionSettings: \`${name}\` is a setting Ablo already ` +
|
|
251
251
|
`manages on its write connection, so a mapping can't reassign it — ` +
|
|
252
252
|
`that would let a schema relax the engine's own scoping. Point your ` +
|
|
253
|
-
`policies at a
|
|
254
|
-
`map Ablo's trusted
|
|
255
|
-
`${
|
|
253
|
+
`policies at a setting your app owns (e.g. \`app.current_org\`) and ` +
|
|
254
|
+
`map Ablo's trusted identity into that instead. Reserved: ` +
|
|
255
|
+
`${RESERVED_SESSION_SETTINGS.join(', ')}.`, { code: 'schema_definition_invalid', param: `sessionSettings.${name}` });
|
|
256
256
|
}
|
|
257
|
-
if (seen.has(name)) {
|
|
258
|
-
throw new AbloValidationError(`[defineSchema] tenantContext: \`${name}\` is mapped more than once. ` +
|
|
259
|
-
`A GUC takes exactly one source — keep the mapping you mean and drop ` +
|
|
260
|
-
`the duplicate.`, { code: 'schema_definition_invalid', param: `tenantContext.${name}` });
|
|
261
|
-
}
|
|
262
|
-
seen.add(name);
|
|
263
257
|
}
|
|
264
258
|
}
|
|
265
259
|
/**
|
package/dist/schema/select.js
CHANGED
|
@@ -52,7 +52,7 @@ export function selectModels(schema, keys) {
|
|
|
52
52
|
models: models,
|
|
53
53
|
validators: validators,
|
|
54
54
|
identityRoles: schema.identityRoles,
|
|
55
|
-
|
|
55
|
+
sessionSettings: schema.sessionSettings,
|
|
56
56
|
// Record the full source's hash so the drift check can recognize this subset
|
|
57
57
|
// as current against a server running that full schema. Prefer the source's
|
|
58
58
|
// OWN `sourceSchemaHash` when it is itself a projection, so a subset-of-a-
|
|
@@ -31,7 +31,7 @@ import type { Tenancy } from './tenancy.js';
|
|
|
31
31
|
import type { ModelResidency } from './residency.js';
|
|
32
32
|
import type { GrantsRef, LoadStrategy, PersistOptions, AutoFillRule, ConflictAxis } from './model.js';
|
|
33
33
|
import type { RelationType } from './relation.js';
|
|
34
|
-
import { type Schema, type IdentityRole, type EntityRole, type
|
|
34
|
+
import { type Schema, type IdentityRole, type EntityRole, type SessionSettings } from './schema.js';
|
|
35
35
|
/** Current schema-JSON envelope version. Bump this on a breaking change to the
|
|
36
36
|
* JSON shape itself — not to a user's schema. */
|
|
37
37
|
declare const SCHEMA_JSON_VERSION: 3;
|
|
@@ -75,8 +75,8 @@ export interface SchemaJSON {
|
|
|
75
75
|
readonly v: typeof SCHEMA_JSON_VERSION;
|
|
76
76
|
readonly models: Record<string, ModelJSON>;
|
|
77
77
|
readonly identityRoles: readonly IdentityRole[];
|
|
78
|
-
/** Optional so schemas pushed before ADR 0011 still parse (defaults to `
|
|
79
|
-
readonly
|
|
78
|
+
/** Optional so schemas pushed before ADR 0011 still parse (defaults to `{}`). */
|
|
79
|
+
readonly sessionSettings?: SessionSettings;
|
|
80
80
|
}
|
|
81
81
|
/**
|
|
82
82
|
* Project a `Schema` to its JSON form. Drops the client-only closures
|
package/dist/schema/serialize.js
CHANGED
|
@@ -93,7 +93,9 @@ export function toSchemaJSON(schema) {
|
|
|
93
93
|
v: SCHEMA_JSON_VERSION,
|
|
94
94
|
models,
|
|
95
95
|
identityRoles: schema.identityRoles,
|
|
96
|
-
...(schema.
|
|
96
|
+
...(Object.keys(schema.sessionSettings).length > 0
|
|
97
|
+
? { sessionSettings: schema.sessionSettings }
|
|
98
|
+
: {}),
|
|
97
99
|
};
|
|
98
100
|
}
|
|
99
101
|
/** Serialize a `Schema` to a JSON string (the `ablo push` payload). */
|
|
@@ -205,7 +207,7 @@ export function fromSchemaJSON(json) {
|
|
|
205
207
|
models: models,
|
|
206
208
|
validators: validators,
|
|
207
209
|
identityRoles: json.identityRoles,
|
|
208
|
-
|
|
210
|
+
sessionSettings: json.sessionSettings ?? {},
|
|
209
211
|
};
|
|
210
212
|
}
|
|
211
213
|
/** Parse a `Schema` from a JSON string (inverse of {@link serializeSchema}). */
|
package/dist/server/commit.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ import type { ParticipantKind, ConfirmationState } from '../schema/syncDeltaRow.
|
|
|
17
17
|
import type { ParticipantRef } from '../wire/delta.js';
|
|
18
18
|
import type { CommitExecutionResultInput } from '../wire/commit.js';
|
|
19
19
|
import type { Environment } from '../environment.js';
|
|
20
|
-
import type { ReadDependency } from '../coordination/schema.js';
|
|
20
|
+
import type { ReadDependency, TrackDependency } from '../coordination/schema.js';
|
|
21
21
|
export interface CommitContext {
|
|
22
22
|
participantId: string;
|
|
23
23
|
/**
|
|
@@ -97,6 +97,13 @@ export interface CommitContext {
|
|
|
97
97
|
* validates only the rows being written. Omit it to check the write targets alone.
|
|
98
98
|
*/
|
|
99
99
|
reads?: ReadDependency[] | null;
|
|
100
|
+
/**
|
|
101
|
+
* Durable read-dependencies to persist for this commit's participant. Each entry
|
|
102
|
+
* is kept in `track_dependencies` and re-checked against every future delta; a
|
|
103
|
+
* later match opens a `StaleNotification` delivered out of band. Distinct from
|
|
104
|
+
* `reads`, which is checked once here and discarded.
|
|
105
|
+
*/
|
|
106
|
+
track?: TrackDependency[] | null;
|
|
100
107
|
}
|
|
101
108
|
/**
|
|
102
109
|
* The server execution receipt persisted in `mutation_log`. Its runtime schema
|
package/dist/surface.d.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* tuple, so it is the one list of model-verb names a generated summary can
|
|
20
20
|
* describe.
|
|
21
21
|
*/
|
|
22
|
-
export declare const PUBLIC_MODEL_VERBS: readonly ["retrieve", "list", "get", "getAll", "getCount", "create", "update", "delete", "claim", "
|
|
22
|
+
export declare const PUBLIC_MODEL_VERBS: readonly ["retrieve", "list", "get", "getAll", "getCount", "create", "update", "delete", "claim", "track", "join", "onChange"];
|
|
23
23
|
/**
|
|
24
24
|
* The option keys accepted by `list`, `getAll`, and `onChange`, matching the
|
|
25
25
|
* keys of {@link LocalReadOptions}. Note that the lifecycle filter is named
|