@abloatai/humans 0.47.0 → 0.49.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/dist/client.d.ts +6 -0
- package/dist/humans.d.ts +1 -1
- package/dist/local/BaseSyncedStore.d.ts +1 -1
- package/dist/local/Database.js +1 -1
- package/dist/local/RuntimeContext.d.ts +1 -1
- package/dist/local/SyncClient.d.ts +1 -1
- package/dist/local/SyncClient.js +3 -3
- package/dist/local/client/createModelProxy.d.ts +7 -1
- package/dist/local/client/createModelProxy.js +146 -39
- package/dist/local/client/options.d.ts +2 -2
- package/dist/local/client/reactiveEngine.js +64 -7
- package/dist/local/client/resourceTypes.d.ts +1 -1
- package/dist/local/client/resourceTypes.js +1 -1
- package/dist/local/client/storeCluster.d.ts +3 -0
- package/dist/local/client/storeCluster.js +4 -2
- package/dist/local/client/storeLifecycle.d.ts +2 -0
- package/dist/local/client/storeLifecycle.js +1 -0
- package/dist/local/client/wsMutationExecutor.d.ts +4 -1
- package/dist/local/client/wsMutationExecutor.js +26 -2
- package/dist/local/context.js +5 -1
- package/dist/local/interfaces/index.d.ts +3 -1
- package/dist/local/mutators/UndoManager.d.ts +1 -1
- package/dist/local/mutators/UndoManager.js +1 -1
- package/dist/local/query/client.js +15 -0
- package/dist/local/query/types.d.ts +14 -0
- package/dist/local/stores/syncAction.d.ts +1 -1
- package/dist/local/stores/syncAction.js +1 -1
- package/dist/local/sync/ConnectionManager.d.ts +1 -1
- package/dist/local/sync/ConnectionManager.js +1 -1
- package/dist/local/sync/OnDemandLoader.d.ts +3 -0
- package/dist/local/sync/OnDemandLoader.js +15 -2
- package/dist/local/sync/SyncWebSocket.d.ts +1 -1
- package/dist/local/sync/SyncWebSocket.js +1 -1
- package/dist/local/sync/commitFrames.d.ts +1 -1
- package/dist/local/sync/commitFrames.js +1 -1
- package/dist/local/sync/contextPorts.d.ts +1 -1
- package/dist/local/sync/contextPorts.js +1 -1
- package/dist/local/sync/credentialLifecycle.d.ts +1 -1
- package/dist/local/sync/credentialLifecycle.js +1 -1
- package/dist/local/sync/initialize.js +2 -2
- package/dist/local/sync/wsFrameHandlers.d.ts +1 -1
- package/dist/local/sync/wsFrameHandlers.js +1 -1
- package/dist/local/transactions/databaseCommitOutbox.d.ts +1 -1
- package/dist/local/transactions/localMutation.d.ts +1 -1
- package/dist/local/transactions/localMutation.js +1 -1
- package/dist/local/transactions/mutations/MutationQueue.d.ts +2 -2
- package/dist/local/transactions/mutations/MutationQueue.js +4 -4
- package/dist/local/transactions/mutations/batchProcessing.d.ts +1 -1
- package/dist/local/transactions/mutations/commitApi.d.ts +1 -1
- package/dist/local/transactions/mutations/commitLane.d.ts +1 -1
- package/dist/local/transactions/mutations/commitPayload.d.ts +2 -1
- package/dist/local/transactions/mutations/commitPayload.js +8 -0
- package/dist/local/transactions/mutations/commitTransport.d.ts +1 -1
- package/dist/local/transactions/mutations/commitTransport.js +1 -1
- package/dist/local/transactions/mutations/durableCommitRestore.d.ts +1 -1
- package/dist/local/transactions/mutations/durableCommitRestore.js +1 -1
- package/dist/local/transactions/mutations/durableWriteStore.d.ts +4 -4
- package/dist/local/transactions/mutations/durableWriteStore.js +3 -3
- package/dist/local/transactions/mutations/localMutation.d.ts +1 -1
- package/dist/local/transactions/mutations/mutationPersistence.d.ts +1 -1
- package/dist/local/transactions/mutations/pendingDrain.d.ts +2 -2
- package/dist/local/transactions/mutations/pendingDrain.js +1 -1
- package/dist/local/transactions/mutations/replayValidation.d.ts +13 -1
- package/dist/local/transactions/mutations/replayValidation.js +3 -2
- package/dist/local/transactions/reconnectDrain.d.ts +1 -1
- package/dist/react/useSyncStatus.d.ts +1 -1
- package/dist/react/useSyncStatus.js +1 -1
- package/package.json +2 -2
- package/src/Ablo.ts +1 -1
- package/src/client.ts +7 -0
- package/src/humans.ts +1 -1
- package/src/local/BaseSyncedStore.ts +1 -1
- package/src/local/Database.ts +1 -1
- package/src/local/RuntimeContext.ts +1 -1
- package/src/local/SyncClient.ts +3 -3
- package/src/local/client/createModelProxy.ts +236 -55
- package/src/local/client/options.ts +2 -2
- package/src/local/client/reactiveEngine.ts +75 -4
- package/src/local/client/resourceTypes.ts +1 -1
- package/src/local/client/storeCluster.ts +12 -2
- package/src/local/client/storeLifecycle.ts +3 -0
- package/src/local/client/wsMutationExecutor.ts +43 -1
- package/src/local/context.ts +5 -1
- package/src/local/interfaces/index.ts +7 -5
- package/src/local/mutators/UndoManager.ts +1 -1
- package/src/local/query/client.ts +17 -0
- package/src/local/query/types.ts +16 -1
- package/src/local/stores/syncAction.ts +1 -1
- package/src/local/sync/ConnectionManager.ts +1 -1
- package/src/local/sync/OnDemandLoader.ts +20 -3
- package/src/local/sync/SyncWebSocket.ts +1 -1
- package/src/local/sync/commitFrames.ts +1 -1
- package/src/local/sync/contextPorts.ts +1 -1
- package/src/local/sync/credentialLifecycle.ts +1 -1
- package/src/local/sync/initialize.ts +2 -2
- package/src/local/sync/wsFrameHandlers.ts +1 -1
- package/src/local/transactions/databaseCommitOutbox.ts +1 -1
- package/src/local/transactions/localMutation.ts +1 -1
- package/src/local/transactions/mutations/MutationQueue.ts +4 -4
- package/src/local/transactions/mutations/batchProcessing.ts +1 -1
- package/src/local/transactions/mutations/commitApi.ts +1 -1
- package/src/local/transactions/mutations/commitLane.ts +1 -1
- package/src/local/transactions/mutations/commitPayload.ts +10 -1
- package/src/local/transactions/mutations/commitTransport.ts +1 -1
- package/src/local/transactions/mutations/durableCommitRestore.ts +2 -2
- package/src/local/transactions/mutations/durableWriteStore.ts +4 -4
- package/src/local/transactions/mutations/localMutation.ts +1 -1
- package/src/local/transactions/mutations/mutationPersistence.ts +1 -1
- package/src/local/transactions/mutations/pendingDrain.ts +2 -2
- package/src/local/transactions/mutations/replayValidation.ts +3 -2
- package/src/local/transactions/reconnectDrain.ts +1 -1
- package/src/react/useSyncStatus.ts +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ import type { SyncWebSocket, CoreSyncEventMap } from './local/sync/SyncWebSocket
|
|
|
18
18
|
import type { SyncStatus } from './local/BaseSyncedStore.js';
|
|
19
19
|
import type { ModelOperations } from './local/client/createModelProxy.js';
|
|
20
20
|
import type { ClaimResource, CommitResource, CreateAgentClientParams, SessionResource } from '@abloatai/transaction/resources/httpResources';
|
|
21
|
+
import type { EffectiveAuthority } from '@abloatai/transaction/auth';
|
|
21
22
|
export type { LocalReadOptions } from './local/client/resourceTypes.js';
|
|
22
23
|
/** The typed sync engine client — one property per model in the schema */
|
|
23
24
|
export type AbloClient<S extends SchemaRecord> = {
|
|
@@ -156,6 +157,11 @@ export type AbloClient<S extends SchemaRecord> = {
|
|
|
156
157
|
* ```
|
|
157
158
|
*/
|
|
158
159
|
readonly organizationId: string | null;
|
|
160
|
+
/**
|
|
161
|
+
* The effective authority of this running participant, confirmed by the
|
|
162
|
+
* server credential exchange/identity endpoint. `null` until `ready()`.
|
|
163
|
+
*/
|
|
164
|
+
readonly identity: EffectiveAuthority | null;
|
|
159
165
|
/**
|
|
160
166
|
* Destroy every IndexedDB database owned by this engine. Disconnects
|
|
161
167
|
* the WebSocket, releases timers, and deletes all `ablo_*` / `ablo-*`
|
package/dist/humans.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Public human-facing local-state capability.
|
|
3
3
|
*
|
|
4
4
|
* The plugin and its local-state runtime belong to @abloatai/humans. The
|
|
5
|
-
* shared
|
|
5
|
+
* shared confirmation package supplies only the handoff identity and wire
|
|
6
6
|
* contracts.
|
|
7
7
|
*/
|
|
8
8
|
import type { PluginContext, AppliedChange } from './plugin.js';
|
|
@@ -118,7 +118,7 @@ export interface UserContext {
|
|
|
118
118
|
* - `'none'`: open the WebSocket and process live deltas only.
|
|
119
119
|
* Reads go through `model.get()` / filtered subscriptions
|
|
120
120
|
* backfilled by `Covering` deltas. Suitable for transactional
|
|
121
|
-
* participants —
|
|
121
|
+
* participants — headless workers, video pipelines, routine runners —
|
|
122
122
|
* that don't need a local replica of the org's tenant plane.
|
|
123
123
|
*/
|
|
124
124
|
bootstrapMode?: 'full' | 'none';
|
package/dist/local/Database.js
CHANGED
|
@@ -791,7 +791,7 @@ export class Database {
|
|
|
791
791
|
// ── inMemory short-circuit ───────────────────────────────────────
|
|
792
792
|
//
|
|
793
793
|
// The batched IDB transaction path below assumes `this.storeManager`
|
|
794
|
-
// and `workspaceDb`. In inMemory mode (
|
|
794
|
+
// and `workspaceDb`. In inMemory mode (headless workers, tests) those
|
|
795
795
|
// don't exist. Without this branch, every live delta arriving over
|
|
796
796
|
// the WebSocket is silently dropped — the local pool never updates,
|
|
797
797
|
// `subscribe()` autoruns never re-fire, lazy-model dispatchers
|
|
@@ -25,7 +25,7 @@ export interface RuntimeContext {
|
|
|
25
25
|
config: RuntimeConfig;
|
|
26
26
|
/**
|
|
27
27
|
* Model metadata lookup. Injected by the client (it wires this from its
|
|
28
|
-
* `ModelRegistry`) so the
|
|
28
|
+
* `ModelRegistry`) so the confirmation core can resolve a model's metadata
|
|
29
29
|
* without importing the registry — the dependency inversion that keeps the
|
|
30
30
|
* core free of the client's `Model` layer (ADR 0013).
|
|
31
31
|
*/
|
|
@@ -316,7 +316,7 @@ export declare class SyncClient extends EventEmitter {
|
|
|
316
316
|
* Called when WebSocket successfully connects (can happen independently of browser online/offline)
|
|
317
317
|
*/
|
|
318
318
|
markConnected(): void;
|
|
319
|
-
private
|
|
319
|
+
private drainPendingConfirmations;
|
|
320
320
|
/**
|
|
321
321
|
* Dispose and cleanup
|
|
322
322
|
*/
|
package/dist/local/SyncClient.js
CHANGED
|
@@ -1029,7 +1029,7 @@ export class SyncClient extends EventEmitter {
|
|
|
1029
1029
|
if (this.pendingStages.size > 0) {
|
|
1030
1030
|
await Promise.all([...this.pendingStages]);
|
|
1031
1031
|
}
|
|
1032
|
-
await this.
|
|
1032
|
+
await this.drainPendingConfirmations();
|
|
1033
1033
|
}
|
|
1034
1034
|
/**
|
|
1035
1035
|
* Resolve a conflict between the local model and incoming server data,
|
|
@@ -1234,7 +1234,7 @@ export class SyncClient extends EventEmitter {
|
|
|
1234
1234
|
// Browser online state may have marked the client connected before the
|
|
1235
1235
|
// WebSocket itself was ready. Always kick both durable lanes on the real
|
|
1236
1236
|
// socket event, even when the high-level state did not change.
|
|
1237
|
-
void this.
|
|
1237
|
+
void this.drainPendingConfirmations().catch((error) => {
|
|
1238
1238
|
this.runtime.observability.captureMutationFailure({
|
|
1239
1239
|
context: 'restore-commit-outbox',
|
|
1240
1240
|
error: error instanceof Error ? error : new Error(String(error)),
|
|
@@ -1242,7 +1242,7 @@ export class SyncClient extends EventEmitter {
|
|
|
1242
1242
|
});
|
|
1243
1243
|
void this.processPendingMutations();
|
|
1244
1244
|
}
|
|
1245
|
-
|
|
1245
|
+
drainPendingConfirmations() {
|
|
1246
1246
|
return this.reconnectDrain.drain(() => this.mutationQueue.drainPending());
|
|
1247
1247
|
}
|
|
1248
1248
|
/**
|
|
@@ -22,6 +22,7 @@ export type { Claim, ClaimHeartbeat, ClaimHeartbeatOptions, HeldClaim, HeldLease
|
|
|
22
22
|
import type { ClaimApi, ClaimAttemptEvent, JoinOptions, LocalCountOptions, LocalReadOptions } from '@abloatai/transaction/resources/modelOperations';
|
|
23
23
|
import type { HttpModelClient } from '@abloatai/transaction/transport/httpClient';
|
|
24
24
|
import type { ParticipantKind } from '@abloatai/transaction/types/participant';
|
|
25
|
+
import { type ReadSetContext } from '@abloatai/transaction/internal/read-set';
|
|
25
26
|
export interface ModelClientMeta {
|
|
26
27
|
readonly key: string;
|
|
27
28
|
readonly typename: string;
|
|
@@ -33,6 +34,11 @@ export declare function getModelClientMeta(modelClient: unknown): ModelClientMet
|
|
|
33
34
|
*/
|
|
34
35
|
type EntityHalf = Pick<ModelTarget, 'model' | 'id'>;
|
|
35
36
|
export interface ModelCollaboration {
|
|
37
|
+
/** Exact point evidence from the HTTP read boundary (stamp captured before data). */
|
|
38
|
+
readPoint(model: string, id: string): Promise<{
|
|
39
|
+
data: unknown;
|
|
40
|
+
stamp: number;
|
|
41
|
+
}>;
|
|
36
42
|
createClaim(options: {
|
|
37
43
|
/**
|
|
38
44
|
* The locator, in the spelling the SDK surface and the HTTP routes use.
|
|
@@ -245,4 +251,4 @@ export declare function createModelProxy<T, C>(schemaKey: string, registeredMode
|
|
|
245
251
|
* collaborator, a test most of all, is pushed into a cast through `unknown`
|
|
246
252
|
* to supply the one method that is actually read.
|
|
247
253
|
*/
|
|
248
|
-
hydration: Pick<OnDemandLoader, 'fetch'
|
|
254
|
+
hydration: Pick<OnDemandLoader, 'fetch'> & Partial<Pick<OnDemandLoader, 'getReadEvidence'>>, collaboration?: ModelCollaboration, readSetContext?: ReadSetContext): ModelOperations<T, C>;
|
|
@@ -27,6 +27,7 @@ import { subTarget } from '@abloatai/transaction/coordination';
|
|
|
27
27
|
import { declaredMeta } from '@abloatai/transaction/coordination/claimMeta';
|
|
28
28
|
import { ModelScope } from '@abloatai/transaction/types';
|
|
29
29
|
import { claimQueueView, resolveClaimContentionOptions, } from '@abloatai/transaction/resources/modelOperations';
|
|
30
|
+
import { abortReadSetCommit, capturePointRead, consumeReadSet, prepareReadSet, } from '@abloatai/transaction/internal/read-set';
|
|
30
31
|
const modelClientMeta = new WeakMap();
|
|
31
32
|
export function getModelClientMeta(modelClient) {
|
|
32
33
|
if (typeof modelClient !== 'object' || modelClient === null)
|
|
@@ -43,7 +44,8 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
43
44
|
* collaborator, a test most of all, is pushed into a cast through `unknown`
|
|
44
45
|
* to supply the one method that is actually read.
|
|
45
46
|
*/
|
|
46
|
-
hydration, collaboration) {
|
|
47
|
+
hydration, collaboration, readSetContext) {
|
|
48
|
+
const readSetClientIdentity = syncClient;
|
|
47
49
|
/**
|
|
48
50
|
* Resolve a row **this** resource owns.
|
|
49
51
|
*
|
|
@@ -152,24 +154,45 @@ hydration, collaboration) {
|
|
|
152
154
|
value.object === 'claim' &&
|
|
153
155
|
typeof value.id === 'string' &&
|
|
154
156
|
typeof value.release === 'function';
|
|
155
|
-
const
|
|
157
|
+
const preparedMutation = (params) => {
|
|
158
|
+
const prepared = prepareReadSet(readSetContext, readSetClientIdentity, params.readAt, params.onStale, params.idempotencyKey, params.reads);
|
|
156
159
|
const rest = {
|
|
157
|
-
...(
|
|
158
|
-
? { idempotencyKey:
|
|
159
|
-
:
|
|
160
|
+
...(prepared.idempotencyKey !== undefined
|
|
161
|
+
? { idempotencyKey: prepared.idempotencyKey }
|
|
162
|
+
: params.idempotencyKey !== undefined
|
|
163
|
+
? { idempotencyKey: params.idempotencyKey }
|
|
164
|
+
: {}),
|
|
160
165
|
...(params.label !== undefined ? { label: params.label } : {}),
|
|
161
|
-
...(
|
|
162
|
-
|
|
166
|
+
...(prepared.readAt !== undefined
|
|
167
|
+
? { readAt: prepared.readAt }
|
|
168
|
+
: params.readAt !== undefined
|
|
169
|
+
? { readAt: params.readAt }
|
|
170
|
+
: {}),
|
|
171
|
+
...(prepared.onStale !== undefined
|
|
172
|
+
? { onStale: prepared.onStale }
|
|
173
|
+
: params.onStale !== undefined
|
|
174
|
+
? { onStale: params.onStale }
|
|
175
|
+
: {}),
|
|
163
176
|
...(params.fenceToken !== undefined ? { fenceToken: params.fenceToken } : {}),
|
|
164
177
|
...(params.claimRef !== undefined ? { claimRef: params.claimRef } : {}),
|
|
165
|
-
...(
|
|
178
|
+
...(prepared.reads !== undefined
|
|
179
|
+
? { reads: prepared.reads === null ? null : [...prepared.reads] }
|
|
180
|
+
: params.reads !== undefined
|
|
181
|
+
? { reads: params.reads }
|
|
182
|
+
: {}),
|
|
166
183
|
...(params.track !== undefined ? { track: params.track } : {}),
|
|
167
184
|
};
|
|
168
185
|
// The write-options schema — the runtime twin of the compile-time params.
|
|
169
186
|
// Catches plain-JavaScript callers (for example `onStale: 'rejct'`) at the
|
|
170
187
|
// call site with a typed error instead of a silent no-op or a server 400.
|
|
171
|
-
|
|
172
|
-
|
|
188
|
+
try {
|
|
189
|
+
assertWriteOptions(rest, `${schemaKey} write`);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
abortReadSetCommit(readSetContext, prepared.automaticCommit);
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
return { options: rest, prepared };
|
|
173
196
|
};
|
|
174
197
|
const releaseClaim = async (id) => {
|
|
175
198
|
const held = activeClaims.get(id);
|
|
@@ -615,18 +638,44 @@ hydration, collaboration) {
|
|
|
615
638
|
where: [['id', params.id]],
|
|
616
639
|
limit: 1,
|
|
617
640
|
});
|
|
641
|
+
if (readSetContext?.getStore() && collaboration) {
|
|
642
|
+
const read = await collaboration.readPoint(schemaKey, params.id);
|
|
643
|
+
const data = (read.data ?? undefined);
|
|
644
|
+
capturePointRead(readSetContext, readSetClientIdentity, wireModel, params.id, data, read.stamp);
|
|
645
|
+
return data;
|
|
646
|
+
}
|
|
618
647
|
return rows[0];
|
|
619
648
|
});
|
|
649
|
+
const list = guard(async (options) => {
|
|
650
|
+
const registry = readSetContext?.getStore();
|
|
651
|
+
const rows = await load(options);
|
|
652
|
+
if (!registry)
|
|
653
|
+
return rows;
|
|
654
|
+
for (const row of rows) {
|
|
655
|
+
const stamp = hydration.getReadEvidence?.(row);
|
|
656
|
+
if (stamp === undefined) {
|
|
657
|
+
// Local-first/lazy rows remain valid reads, but cannot later be used as
|
|
658
|
+
// guarded dependencies. `prepareReadSet` rejects them if supplied in
|
|
659
|
+
// `reads`; authoritative complete reads carry evidence here.
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
const id = row.id;
|
|
663
|
+
if (typeof id !== 'string' || id.length === 0) {
|
|
664
|
+
throw new AbloValidationError(`${registeredModelName}.list returned a row without an id.`, { code: 'commit_no_result' });
|
|
665
|
+
}
|
|
666
|
+
capturePointRead(readSetContext, readSetClientIdentity, wireModel, id, row, stamp);
|
|
667
|
+
}
|
|
668
|
+
return rows;
|
|
669
|
+
});
|
|
620
670
|
const operations = {
|
|
621
671
|
local,
|
|
622
672
|
get,
|
|
623
673
|
retrieve: get,
|
|
624
674
|
// No automatic scope enrolment on bulk `list`: that would subscribe to an
|
|
625
675
|
// unbounded set of rows' entity groups.
|
|
626
|
-
list
|
|
676
|
+
list,
|
|
627
677
|
create: guardWrite(async (params) => {
|
|
628
678
|
const id = params.id ?? Model.generateId();
|
|
629
|
-
const opts = mutationOptions(params);
|
|
630
679
|
const claim = params.claim;
|
|
631
680
|
let autoLease;
|
|
632
681
|
if (claim && !isClaimHandle(claim)) {
|
|
@@ -668,16 +717,38 @@ hydration, collaboration) {
|
|
|
668
717
|
createdAt: new Date(),
|
|
669
718
|
updatedAt: new Date(),
|
|
670
719
|
});
|
|
671
|
-
|
|
672
|
-
...opts,
|
|
673
|
-
...(autoLease ? { claim: autoLease } : {}),
|
|
674
|
-
...(isClaimHandle(claim) ? { claim: { id: claim.id } } : {}),
|
|
675
|
-
};
|
|
720
|
+
let prepared;
|
|
676
721
|
try {
|
|
722
|
+
const resolved = preparedMutation(params);
|
|
723
|
+
prepared = resolved.prepared;
|
|
724
|
+
const effective = {
|
|
725
|
+
...resolved.options,
|
|
726
|
+
...(autoLease
|
|
727
|
+
? {
|
|
728
|
+
claimRef: { id: autoLease.id },
|
|
729
|
+
...(autoLease.fenceToken !== undefined
|
|
730
|
+
? { fenceToken: autoLease.fenceToken }
|
|
731
|
+
: {}),
|
|
732
|
+
}
|
|
733
|
+
: {}),
|
|
734
|
+
...(isClaimHandle(claim)
|
|
735
|
+
? {
|
|
736
|
+
claimRef: { id: claim.id },
|
|
737
|
+
...(claim.fenceToken !== undefined
|
|
738
|
+
? { fenceToken: claim.fenceToken }
|
|
739
|
+
: {}),
|
|
740
|
+
}
|
|
741
|
+
: {}),
|
|
742
|
+
};
|
|
677
743
|
syncClient.add(model, effective);
|
|
678
744
|
await waitForMutation(model);
|
|
745
|
+
consumeReadSet(readSetContext, readSetClientIdentity, prepared.consumed, prepared.automaticCommit);
|
|
679
746
|
return modelAsRow(model);
|
|
680
747
|
}
|
|
748
|
+
catch (error) {
|
|
749
|
+
abortReadSetCommit(readSetContext, prepared?.automaticCommit ?? false);
|
|
750
|
+
throw error;
|
|
751
|
+
}
|
|
681
752
|
finally {
|
|
682
753
|
await autoLease?.release?.().catch(() => { });
|
|
683
754
|
}
|
|
@@ -712,11 +783,11 @@ hydration, collaboration) {
|
|
|
712
783
|
// `type: 'complete'` forces the round-trip — the hydration ledger
|
|
713
784
|
// would otherwise serve a possibly-stale local row for a hydrated id.
|
|
714
785
|
await load({ where: [['id', id]], type: 'complete' });
|
|
715
|
-
const
|
|
716
|
-
|
|
786
|
+
const exact = await collaboration.readPoint(wireModel, id);
|
|
787
|
+
capturePointRead(readSetContext, readSetClientIdentity, wireModel, id, exact.data, exact.stamp);
|
|
717
788
|
return {
|
|
718
|
-
data:
|
|
719
|
-
stamp:
|
|
789
|
+
data: exact.data,
|
|
790
|
+
stamp: exact.stamp,
|
|
720
791
|
};
|
|
721
792
|
},
|
|
722
793
|
writeNext: async (patch, readAt) => {
|
|
@@ -724,14 +795,28 @@ hydration, collaboration) {
|
|
|
724
795
|
if (!model) {
|
|
725
796
|
throw new AbloValidationError(`Entity not found: ${registeredModelName}/${id}`, { code: 'entity_not_found' });
|
|
726
797
|
}
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
798
|
+
const prepared = prepareReadSet(readSetContext, readSetClientIdentity, undefined, 'reject', undefined, contention?.reads);
|
|
799
|
+
try {
|
|
800
|
+
const effective = {
|
|
801
|
+
readAt: prepared.readAt ?? readAt,
|
|
802
|
+
onStale: prepared.onStale ?? 'reject',
|
|
803
|
+
...(prepared.idempotencyKey
|
|
804
|
+
? { idempotencyKey: prepared.idempotencyKey }
|
|
805
|
+
: {}),
|
|
806
|
+
...(prepared.reads !== undefined
|
|
807
|
+
? { reads: prepared.reads === null ? null : [...prepared.reads] }
|
|
808
|
+
: {}),
|
|
809
|
+
};
|
|
810
|
+
model.applyChanges(patch);
|
|
811
|
+
syncClient.update(model, effective);
|
|
812
|
+
await waitForMutation(model);
|
|
813
|
+
consumeReadSet(readSetContext, readSetClientIdentity, prepared.consumed, prepared.automaticCommit);
|
|
814
|
+
return modelAsRow(model);
|
|
815
|
+
}
|
|
816
|
+
catch (error) {
|
|
817
|
+
abortReadSetCommit(readSetContext, prepared.automaticCommit);
|
|
818
|
+
throw error;
|
|
819
|
+
}
|
|
735
820
|
},
|
|
736
821
|
});
|
|
737
822
|
}
|
|
@@ -758,13 +843,17 @@ hydration, collaboration) {
|
|
|
758
843
|
// If we hold a claim on this row, guard the write with its snapshot
|
|
759
844
|
// watermark + lease so it's stale-rejected and attributed to the claim.
|
|
760
845
|
const claimed = activeClaims.get(id);
|
|
761
|
-
const
|
|
846
|
+
const resolved = preparedMutation(params);
|
|
847
|
+
const opts = resolved.options;
|
|
762
848
|
const handle = isClaimHandle(params.claim) ? params.claim : undefined;
|
|
763
849
|
const effective = claimed
|
|
764
850
|
? {
|
|
765
851
|
readAt: claimed.lease.readAt ?? claimed.snapshot.stamp,
|
|
766
852
|
onStale: 'reject',
|
|
767
853
|
claimRef: { id: claimed.lease.id },
|
|
854
|
+
...(claimed.lease.fenceToken !== undefined
|
|
855
|
+
? { fenceToken: claimed.lease.fenceToken }
|
|
856
|
+
: {}),
|
|
768
857
|
...opts,
|
|
769
858
|
}
|
|
770
859
|
: {
|
|
@@ -781,16 +870,23 @@ hydration, collaboration) {
|
|
|
781
870
|
}
|
|
782
871
|
: {}),
|
|
783
872
|
...opts,
|
|
784
|
-
...(handle ? {
|
|
873
|
+
...(handle ? { claimRef: { id: handle.id } } : {}),
|
|
785
874
|
};
|
|
786
875
|
// Local user update: `applyChanges` keeps change tracking on so the
|
|
787
876
|
// edited fields land in `modifiedProperties` and are actually sent to
|
|
788
877
|
// the server. (`updateFromData` is the hydration path and would discard
|
|
789
878
|
// the tracking, producing an empty `input: {}` no-op mutation.)
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
879
|
+
try {
|
|
880
|
+
model.applyChanges(params.data);
|
|
881
|
+
syncClient.update(model, effective);
|
|
882
|
+
await waitForMutation(model);
|
|
883
|
+
consumeReadSet(readSetContext, readSetClientIdentity, resolved.prepared.consumed, resolved.prepared.automaticCommit);
|
|
884
|
+
return modelAsRow(model);
|
|
885
|
+
}
|
|
886
|
+
catch (error) {
|
|
887
|
+
abortReadSetCommit(readSetContext, resolved.prepared.automaticCommit);
|
|
888
|
+
throw error;
|
|
889
|
+
}
|
|
794
890
|
});
|
|
795
891
|
function update(arg, updater, contention) {
|
|
796
892
|
return updateImpl(arg, updater, contention);
|
|
@@ -826,7 +922,8 @@ hydration, collaboration) {
|
|
|
826
922
|
if (!model)
|
|
827
923
|
return;
|
|
828
924
|
const claimed = activeClaims.get(id);
|
|
829
|
-
const
|
|
925
|
+
const resolved = preparedMutation(params);
|
|
926
|
+
const opts = resolved.options;
|
|
830
927
|
const handle = isClaimHandle(params.claim) ? params.claim : undefined;
|
|
831
928
|
const effective = claimed
|
|
832
929
|
? {
|
|
@@ -843,13 +940,23 @@ hydration, collaboration) {
|
|
|
843
940
|
? {
|
|
844
941
|
readAt: handle.readAt,
|
|
845
942
|
onStale: 'reject',
|
|
943
|
+
...(handle.fenceToken !== undefined
|
|
944
|
+
? { fenceToken: handle.fenceToken }
|
|
945
|
+
: {}),
|
|
846
946
|
}
|
|
847
947
|
: {}),
|
|
848
948
|
...opts,
|
|
849
|
-
...(handle ? {
|
|
949
|
+
...(handle ? { claimRef: { id: handle.id } } : {}),
|
|
850
950
|
};
|
|
851
|
-
|
|
852
|
-
|
|
951
|
+
try {
|
|
952
|
+
syncClient.delete(model, effective);
|
|
953
|
+
await waitForMutation(model);
|
|
954
|
+
consumeReadSet(readSetContext, readSetClientIdentity, resolved.prepared.consumed, resolved.prepared.automaticCommit);
|
|
955
|
+
}
|
|
956
|
+
catch (error) {
|
|
957
|
+
abortReadSetCommit(readSetContext, resolved.prepared.automaticCommit);
|
|
958
|
+
throw error;
|
|
959
|
+
}
|
|
853
960
|
}),
|
|
854
961
|
// `claim` is a callable namespace (take a claim) carrying the coordination
|
|
855
962
|
// readers (`claim.state` / `claim.queue` / `claim.release` / `claim.reorder`).
|
|
@@ -8,7 +8,7 @@ import type { Schema, SchemaRecord } from '@abloatai/transaction/schema/schema';
|
|
|
8
8
|
import type { RuntimeConfig, Logger, MutationExecutor, ObservabilityProvider, Analytics, SessionErrorDetector, OnlineStatusProvider } from '../interfaces/index.js';
|
|
9
9
|
import type { AbloPersistence } from '../persistence.js';
|
|
10
10
|
import type { DurableWriteStore, DurableWritesConfig } from '@abloatai/transaction/durableWrites';
|
|
11
|
-
import type { CommitOutboxScope } from '@abloatai/transaction/transactions/
|
|
11
|
+
import type { CommitOutboxScope } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
|
|
12
12
|
/**
|
|
13
13
|
* An async function that resolves an apiKey at request time. Use it for credential
|
|
14
14
|
* rotation — read from a vault, refresh from session storage, or pull from an
|
|
@@ -129,7 +129,7 @@ export interface AbloOptions<S extends SchemaRecord = SchemaRecord> {
|
|
|
129
129
|
* Ablo({
|
|
130
130
|
* schema,
|
|
131
131
|
* apiKey,
|
|
132
|
-
* durableWrites: { store, namespace: '
|
|
132
|
+
* durableWrites: { store, namespace: 'headless-worker' },
|
|
133
133
|
* })
|
|
134
134
|
* ```
|
|
135
135
|
*/
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
* design step (docs/plans/package-split.md).
|
|
13
13
|
*/
|
|
14
14
|
import { omittedModelError } from '@abloatai/transaction/schema/select';
|
|
15
|
-
import { durableCommitOperationSchema, } from '@abloatai/transaction/transactions/
|
|
15
|
+
import { durableCommitOperationSchema, } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
|
|
16
16
|
import { AbloAuthenticationError, AbloConnectionError, AbloValidationError, claimedError } from '@abloatai/transaction/errors';
|
|
17
|
-
import { batchFence, fenceTokenFor, modelTarget, streamTarget, subTarget, } from '@abloatai/transaction/coordination';
|
|
17
|
+
import { batchFence, claimIdFor, fenceTokenFor, modelTarget, streamTarget, subTarget, } from '@abloatai/transaction/coordination';
|
|
18
18
|
import { validateAbloOptions } from './validateAbloOptions.js';
|
|
19
19
|
import { mintSession } from '@abloatai/transaction/auth/sessionMint';
|
|
20
20
|
import { revokeCapability, rotateCapability, } from '@abloatai/transaction/auth/capabilityLifecycle';
|
|
@@ -28,14 +28,41 @@ import { resolveApiKeyValue, resolveBootstrapBaseUrl } from '@abloatai/transacti
|
|
|
28
28
|
import { claimAttemptFailure, emitClaimStatus, } from '@abloatai/transaction/resources/modelOperations';
|
|
29
29
|
import { createModelProxy } from './createModelProxy.js';
|
|
30
30
|
import { assertWriteOptions } from '@abloatai/transaction/resources/writeOptionsSchema';
|
|
31
|
+
import { modelReadResponseSchema, commitRecordSchema, commitRecordListSchema, commitRecordWhereSchema, } from '@abloatai/transaction/wire';
|
|
32
|
+
import { translateHttpError } from '@abloatai/transaction/errors';
|
|
31
33
|
export function buildReactiveEngine(inputs) {
|
|
32
34
|
const { options, internalOptions, url, logger, configuredApiKey, configuredAuthToken, credentialResolver, authCredentials, transport, participantId, kind, presence, cluster, createSibling, } = inputs;
|
|
33
35
|
const schema = options.schema;
|
|
36
|
+
const pointReadBaseUrl = resolveBootstrapBaseUrl({
|
|
37
|
+
url,
|
|
38
|
+
bootstrapBaseUrl: internalOptions.bootstrapBaseUrl,
|
|
39
|
+
});
|
|
40
|
+
async function readPoint(model, id) {
|
|
41
|
+
const fetchImpl = internalOptions.fetch ?? globalThis.fetch;
|
|
42
|
+
const response = await fetchImpl(`${pointReadBaseUrl}/v1/models/${encodeURIComponent(model)}/${encodeURIComponent(id)}`, { headers: authCredentials.withAuthHeaders() });
|
|
43
|
+
let body;
|
|
44
|
+
try {
|
|
45
|
+
body = await response.json();
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
body = null;
|
|
49
|
+
}
|
|
50
|
+
if (!response.ok)
|
|
51
|
+
throw translateHttpError(response.status, body);
|
|
52
|
+
const parsed = modelReadResponseSchema.safeParse(body);
|
|
53
|
+
if (!parsed.success) {
|
|
54
|
+
throw new AbloConnectionError('Model point-read response failed validation.', {
|
|
55
|
+
code: 'commit_no_result',
|
|
56
|
+
cause: parsed.error,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return { data: parsed.data.data, stamp: parsed.data.stamp };
|
|
60
|
+
}
|
|
34
61
|
// The store cluster — this client's runtime, the component graph, the
|
|
35
62
|
// registered models, and the store — was constructed by `humans().init`
|
|
36
63
|
// from the widened plugin context (see `./storeCluster.ts`). The engine
|
|
37
64
|
// assembles around it.
|
|
38
|
-
const { components, store } = cluster;
|
|
65
|
+
const { components, store, readSetContext } = cluster;
|
|
39
66
|
const { modelRegistry, objectPool, syncClient, hydration, } = components;
|
|
40
67
|
// Self identity, late-bound the same way the connection's values are: the
|
|
41
68
|
// construction-time guess seeds it (correct on the self-hosted path, empty
|
|
@@ -93,6 +120,7 @@ export function buildReactiveEngine(inputs) {
|
|
|
93
120
|
/** Resolved account scope — seeded once identity resolution completes;
|
|
94
121
|
* exposed as the readonly `ablo.organizationId` accessor. */
|
|
95
122
|
let _resolvedOrganizationId = null;
|
|
123
|
+
let _resolvedIdentity = null;
|
|
96
124
|
const lifecycle = startStoreLifecycle({
|
|
97
125
|
cluster,
|
|
98
126
|
schema,
|
|
@@ -105,10 +133,11 @@ export function buildReactiveEngine(inputs) {
|
|
|
105
133
|
kind,
|
|
106
134
|
logger,
|
|
107
135
|
validationError: _validationError,
|
|
108
|
-
onIdentityResolved: ({ userId, participantKind, accountScope, syncGroups }) => {
|
|
136
|
+
onIdentityResolved: ({ userId, participantKind, accountScope, syncGroups, authority }) => {
|
|
109
137
|
selfParticipantId = userId;
|
|
110
138
|
selfParticipantKind = participantKind;
|
|
111
139
|
_resolvedOrganizationId = accountScope;
|
|
140
|
+
_resolvedIdentity = authority;
|
|
112
141
|
presenceStream.setParticipant({
|
|
113
142
|
id: userId,
|
|
114
143
|
kind: participantKind,
|
|
@@ -145,7 +174,7 @@ export function buildReactiveEngine(inputs) {
|
|
|
145
174
|
? crypto.randomUUID()
|
|
146
175
|
: `tx_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
147
176
|
}
|
|
148
|
-
function normalizeCommitOperation(op, defaults, fence) {
|
|
177
|
+
function normalizeCommitOperation(op, defaults, fence, claim) {
|
|
149
178
|
const type = op.action.toUpperCase();
|
|
150
179
|
const id = op.id ?? '';
|
|
151
180
|
return durableCommitOperationSchema.parse({
|
|
@@ -154,6 +183,7 @@ export function buildReactiveEngine(inputs) {
|
|
|
154
183
|
id,
|
|
155
184
|
input: op.data ?? undefined,
|
|
156
185
|
transactionId: op.transactionId ?? undefined,
|
|
186
|
+
claimId: op.claimId ?? claimIdFor(claim?.target, claim?.id, op.model, op.id ?? null) ?? undefined,
|
|
157
187
|
readAt: op.readAt ?? defaults.readAt ?? undefined,
|
|
158
188
|
onStale: op.onStale ?? defaults.onStale ?? undefined,
|
|
159
189
|
fenceToken: op.fenceToken ?? fenceTokenFor(fence, op.model, op.id ?? null) ?? undefined,
|
|
@@ -163,7 +193,7 @@ export function buildReactiveEngine(inputs) {
|
|
|
163
193
|
if (commitOptions.operations.length === 0) {
|
|
164
194
|
throw new AbloValidationError('Commit requires a non-empty `operations` array.', { code: 'commit_operation_required' });
|
|
165
195
|
}
|
|
166
|
-
return commitOptions.operations.map((op) => normalizeCommitOperation(op, commitOptions, fence));
|
|
196
|
+
return commitOptions.operations.map((op) => normalizeCommitOperation(op, commitOptions, fence, commitOptions.claim ?? null));
|
|
167
197
|
}
|
|
168
198
|
function modelClaimFromActive(claim) {
|
|
169
199
|
const target = {
|
|
@@ -377,6 +407,7 @@ export function buildReactiveEngine(inputs) {
|
|
|
377
407
|
const registeredModelName = modelDef.typename ?? schemaKey;
|
|
378
408
|
modelProxies[schemaKey] = createModelProxy(schemaKey, registeredModelName, objectPool, syncClient, modelRegistry, hydration, {
|
|
379
409
|
createClaim: (claimOptions) => publicClaims.create(claimOptions),
|
|
410
|
+
readPoint,
|
|
380
411
|
createSnapshot: (modelKey, id) => createSnapshot({
|
|
381
412
|
pool: objectPool,
|
|
382
413
|
transport,
|
|
@@ -429,7 +460,7 @@ export function buildReactiveEngine(inputs) {
|
|
|
429
460
|
scope: { [modelKey]: ids },
|
|
430
461
|
...(options?.ttl !== undefined ? { ttl: options.ttl } : {}),
|
|
431
462
|
}),
|
|
432
|
-
});
|
|
463
|
+
}, readSetContext);
|
|
433
464
|
}
|
|
434
465
|
const commits = {
|
|
435
466
|
async create(commitOptions) {
|
|
@@ -479,6 +510,29 @@ export function buildReactiveEngine(inputs) {
|
|
|
479
510
|
...(missingIds && missingIds.length > 0 ? { missingIds } : {}),
|
|
480
511
|
};
|
|
481
512
|
},
|
|
513
|
+
async get({ id }) {
|
|
514
|
+
await ready();
|
|
515
|
+
const response = await (internalOptions.fetch ?? globalThis.fetch)(`${pointReadBaseUrl}/v1/commits/${encodeURIComponent(id)}`, { headers: authCredentials.withAuthHeaders() });
|
|
516
|
+
const body = await response.json().catch(() => null);
|
|
517
|
+
if (!response.ok)
|
|
518
|
+
throw translateHttpError(response.status, body);
|
|
519
|
+
return commitRecordSchema.nullable().parse(body);
|
|
520
|
+
},
|
|
521
|
+
async list(listOptions = {}) {
|
|
522
|
+
await ready();
|
|
523
|
+
const where = commitRecordWhereSchema.parse(listOptions.where ?? {});
|
|
524
|
+
const params = new URLSearchParams();
|
|
525
|
+
if (where.actorId)
|
|
526
|
+
params.set('actorId', where.actorId);
|
|
527
|
+
if (where.status)
|
|
528
|
+
params.set('status', where.status);
|
|
529
|
+
const query = params.size > 0 ? `?${params.toString()}` : '';
|
|
530
|
+
const response = await (internalOptions.fetch ?? globalThis.fetch)(`${pointReadBaseUrl}/v1/commits${query}`, { headers: authCredentials.withAuthHeaders() });
|
|
531
|
+
const body = await response.json().catch(() => null);
|
|
532
|
+
if (!response.ok)
|
|
533
|
+
throw translateHttpError(response.status, body);
|
|
534
|
+
return commitRecordListSchema.parse(body);
|
|
535
|
+
},
|
|
482
536
|
};
|
|
483
537
|
/**
|
|
484
538
|
* The control-plane credential: always the original configured secret key.
|
|
@@ -560,6 +614,9 @@ export function buildReactiveEngine(inputs) {
|
|
|
560
614
|
get organizationId() {
|
|
561
615
|
return _resolvedOrganizationId;
|
|
562
616
|
},
|
|
617
|
+
get identity() {
|
|
618
|
+
return _resolvedIdentity;
|
|
619
|
+
},
|
|
563
620
|
nudgeReconnect() {
|
|
564
621
|
store.nudgeReconnect();
|
|
565
622
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The interactive resource-type surface.
|
|
3
3
|
*
|
|
4
|
-
* The transport-facing half of these types moved down into the
|
|
4
|
+
* The transport-facing half of these types moved down into the confirmation core
|
|
5
5
|
* (ADR 0016). This module keeps the
|
|
6
6
|
* consumer-facing import path intact and rejoins it with the model-proxy types
|
|
7
7
|
* that stay here — `ModelOperations` chief among them, because it returns the
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The interactive resource-type surface.
|
|
3
3
|
*
|
|
4
|
-
* The transport-facing half of these types moved down into the
|
|
4
|
+
* The transport-facing half of these types moved down into the confirmation core
|
|
5
5
|
* (ADR 0016). This module keeps the
|
|
6
6
|
* consumer-facing import path intact and rejoins it with the model-proxy types
|
|
7
7
|
* that stay here — `ModelOperations` chief among them, because it returns the
|
|
@@ -19,6 +19,7 @@ import { type RuntimeContext } from '../RuntimeContext.js';
|
|
|
19
19
|
import { BaseSyncedStore } from '../BaseSyncedStore.js';
|
|
20
20
|
import { type InternalComponents } from './createInternalComponents.js';
|
|
21
21
|
import type { InternalAbloOptions } from './options.js';
|
|
22
|
+
import { type ReadSetContext } from '@abloatai/transaction/internal/read-set';
|
|
22
23
|
export type { InternalAbloOptions } from './options.js';
|
|
23
24
|
/**
|
|
24
25
|
* The private handoff slot on the humans surface. Symbol-keyed on purpose:
|
|
@@ -34,6 +35,8 @@ export interface StoreCluster {
|
|
|
34
35
|
readonly components: InternalComponents;
|
|
35
36
|
/** The store orchestrating the graph, holding the host-built connection. */
|
|
36
37
|
readonly store: BaseSyncedStore;
|
|
38
|
+
/** Client-local opaque evidence registry; never an ambient execution scope. */
|
|
39
|
+
readonly readSetContext: ReadSetContext;
|
|
37
40
|
}
|
|
38
41
|
/**
|
|
39
42
|
* Builds the cluster from what the context carries, or returns `null` when
|