@abloatai/humans 0.48.0 → 0.50.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/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 +1 -1
- package/dist/local/client/reactiveEngine.js +69 -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/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/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 +1 -1
- package/src/local/client/reactiveEngine.ts +81 -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/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
|
@@ -20,11 +20,12 @@ import {
|
|
|
20
20
|
import {
|
|
21
21
|
reconcileFunctionalUpdate,
|
|
22
22
|
type ModelUpdater,
|
|
23
|
-
type
|
|
23
|
+
type FunctionalUpdateOptions,
|
|
24
24
|
} from '@abloatai/transaction/resources/functionalUpdate';
|
|
25
25
|
import type { MutationOptions } from '../interfaces/index.js';
|
|
26
26
|
import {
|
|
27
27
|
claimDescription,
|
|
28
|
+
type ReadDependency,
|
|
28
29
|
type TrackDependency,
|
|
29
30
|
} from '@abloatai/transaction/coordination/schema';
|
|
30
31
|
import { Model, modelAsRow } from '../Model.js';
|
|
@@ -63,7 +64,7 @@ import type {
|
|
|
63
64
|
} from '@abloatai/transaction/types/streams';
|
|
64
65
|
|
|
65
66
|
// The request contract — every option and parameter shape a caller passes to a
|
|
66
|
-
// read, a write, or a claim — lives in the
|
|
67
|
+
// read, a write, or a claim — lives in the confirmation core (ADR 0016). This
|
|
67
68
|
// factory binds it to reactive model instances; the shapes themselves are
|
|
68
69
|
// transport- and consumer-agnostic. Re-exported so `./createModelProxy` stays a
|
|
69
70
|
// working import path for the whole surface.
|
|
@@ -122,8 +123,19 @@ import {
|
|
|
122
123
|
claimQueueView,
|
|
123
124
|
resolveClaimContentionOptions,
|
|
124
125
|
} from '@abloatai/transaction/resources/modelOperations';
|
|
125
|
-
import type {
|
|
126
|
+
import type {
|
|
127
|
+
CapturedRow,
|
|
128
|
+
HttpModelClient,
|
|
129
|
+
} from '@abloatai/transaction/transport/httpClient';
|
|
126
130
|
import type { ParticipantKind } from '@abloatai/transaction/types/participant';
|
|
131
|
+
import {
|
|
132
|
+
abortReadSetCommit,
|
|
133
|
+
capturePointRead,
|
|
134
|
+
consumeReadSet,
|
|
135
|
+
prepareReadSet,
|
|
136
|
+
type PreparedReadSet,
|
|
137
|
+
type ReadSetContext,
|
|
138
|
+
} from '@abloatai/transaction/internal/read-set';
|
|
127
139
|
|
|
128
140
|
export interface ModelClientMeta {
|
|
129
141
|
readonly key: string;
|
|
@@ -150,6 +162,8 @@ type EntityHalf = Pick<ModelTarget, 'model' | 'id'>;
|
|
|
150
162
|
// `ModelCollaboration<Task>` and `ModelCollaboration<Invoice>` the same type
|
|
151
163
|
// while reading as though they differed.
|
|
152
164
|
export interface ModelCollaboration {
|
|
165
|
+
/** Exact point evidence from the HTTP read boundary (stamp captured before data). */
|
|
166
|
+
readPoint(model: string, id: string): Promise<{ data: unknown; stamp: number }>;
|
|
153
167
|
createClaim(options: {
|
|
154
168
|
/**
|
|
155
169
|
* The locator, in the spelling the SDK surface and the HTTP routes use.
|
|
@@ -403,9 +417,12 @@ export function createModelProxy<T, C>(
|
|
|
403
417
|
* collaborator, a test most of all, is pushed into a cast through `unknown`
|
|
404
418
|
* to supply the one method that is actually read.
|
|
405
419
|
*/
|
|
406
|
-
hydration: Pick<OnDemandLoader, 'fetch'
|
|
420
|
+
hydration: Pick<OnDemandLoader, 'fetch'> &
|
|
421
|
+
Partial<Pick<OnDemandLoader, 'getReadEvidence'>>,
|
|
407
422
|
collaboration?: ModelCollaboration,
|
|
423
|
+
readSetContext?: ReadSetContext,
|
|
408
424
|
): ModelOperations<T, C> {
|
|
425
|
+
const readSetClientIdentity = syncClient as object;
|
|
409
426
|
/**
|
|
410
427
|
* Resolve a row **this** resource owns.
|
|
411
428
|
*
|
|
@@ -541,29 +558,56 @@ export function createModelProxy<T, C>(
|
|
|
541
558
|
typeof (value as { id?: unknown }).id === 'string' &&
|
|
542
559
|
typeof (value as { release?: unknown }).release === 'function';
|
|
543
560
|
|
|
544
|
-
const
|
|
561
|
+
const preparedMutation = (
|
|
545
562
|
params:
|
|
546
563
|
| ModelCreateParams<T, C>
|
|
547
564
|
| ModelUpdateParams<T, C>
|
|
548
565
|
| ModelDeleteParams<T, C>,
|
|
549
|
-
): MutationOptions => {
|
|
566
|
+
): { options: MutationOptions; prepared: PreparedReadSet } => {
|
|
567
|
+
const prepared = prepareReadSet(
|
|
568
|
+
readSetContext,
|
|
569
|
+
readSetClientIdentity,
|
|
570
|
+
params.readAt,
|
|
571
|
+
params.onStale,
|
|
572
|
+
params.idempotencyKey,
|
|
573
|
+
params.reads,
|
|
574
|
+
);
|
|
550
575
|
const rest: MutationOptions = {
|
|
551
|
-
...(
|
|
552
|
-
? { idempotencyKey:
|
|
576
|
+
...(prepared.idempotencyKey !== undefined
|
|
577
|
+
? { idempotencyKey: prepared.idempotencyKey }
|
|
578
|
+
: params.idempotencyKey !== undefined
|
|
579
|
+
? { idempotencyKey: params.idempotencyKey }
|
|
553
580
|
: {}),
|
|
554
581
|
...(params.label !== undefined ? { label: params.label } : {}),
|
|
555
|
-
...(
|
|
556
|
-
|
|
582
|
+
...(prepared.readAt !== undefined
|
|
583
|
+
? { readAt: prepared.readAt }
|
|
584
|
+
: params.readAt !== undefined
|
|
585
|
+
? { readAt: params.readAt }
|
|
586
|
+
: {}),
|
|
587
|
+
...(prepared.onStale !== undefined
|
|
588
|
+
? { onStale: prepared.onStale }
|
|
589
|
+
: params.onStale !== undefined
|
|
590
|
+
? { onStale: params.onStale }
|
|
591
|
+
: {}),
|
|
557
592
|
...(params.fenceToken !== undefined ? { fenceToken: params.fenceToken } : {}),
|
|
558
593
|
...(params.claimRef !== undefined ? { claimRef: params.claimRef } : {}),
|
|
559
|
-
...(
|
|
594
|
+
...(prepared.reads !== undefined
|
|
595
|
+
? { reads: prepared.reads === null ? null : [...prepared.reads] }
|
|
596
|
+
: params.reads !== undefined
|
|
597
|
+
? { reads: params.reads }
|
|
598
|
+
: {}),
|
|
560
599
|
...(params.track !== undefined ? { track: params.track } : {}),
|
|
561
600
|
};
|
|
562
601
|
// The write-options schema — the runtime twin of the compile-time params.
|
|
563
602
|
// Catches plain-JavaScript callers (for example `onStale: 'rejct'`) at the
|
|
564
603
|
// call site with a typed error instead of a silent no-op or a server 400.
|
|
565
|
-
|
|
566
|
-
|
|
604
|
+
try {
|
|
605
|
+
assertWriteOptions(rest, `${schemaKey} write`);
|
|
606
|
+
} catch (error) {
|
|
607
|
+
abortReadSetCommit(readSetContext, prepared.automaticCommit);
|
|
608
|
+
throw error;
|
|
609
|
+
}
|
|
610
|
+
return { options: rest, prepared };
|
|
567
611
|
};
|
|
568
612
|
|
|
569
613
|
const releaseClaim = async (id: string): Promise<void> => {
|
|
@@ -1074,7 +1118,7 @@ export function createModelProxy<T, C>(
|
|
|
1074
1118
|
};
|
|
1075
1119
|
|
|
1076
1120
|
const get = guard(
|
|
1077
|
-
async (params: ModelRetrieveParams): Promise<T | undefined> => {
|
|
1121
|
+
async (params: ModelRetrieveParams): Promise<CapturedRow<T> | undefined> => {
|
|
1078
1122
|
// Read-interest enrolment: authoritative point reads enter the same
|
|
1079
1123
|
// entity scope as the claim stream, while remaining settled reads.
|
|
1080
1124
|
void collaboration?.enterScope?.({ [schemaKey]: params.id });
|
|
@@ -1083,10 +1127,56 @@ export function createModelProxy<T, C>(
|
|
|
1083
1127
|
where: [['id', params.id]],
|
|
1084
1128
|
limit: 1,
|
|
1085
1129
|
});
|
|
1086
|
-
|
|
1130
|
+
if (readSetContext?.getStore() && collaboration) {
|
|
1131
|
+
const read = await collaboration.readPoint(schemaKey, params.id);
|
|
1132
|
+
const data = (read.data ?? undefined) as T | undefined;
|
|
1133
|
+
capturePointRead(
|
|
1134
|
+
readSetContext,
|
|
1135
|
+
readSetClientIdentity,
|
|
1136
|
+
wireModel,
|
|
1137
|
+
params.id,
|
|
1138
|
+
data,
|
|
1139
|
+
read.stamp,
|
|
1140
|
+
);
|
|
1141
|
+
return data as CapturedRow<T> | undefined;
|
|
1142
|
+
}
|
|
1143
|
+
return rows[0] as CapturedRow<T> | undefined;
|
|
1087
1144
|
},
|
|
1088
1145
|
);
|
|
1089
1146
|
|
|
1147
|
+
const list = guard(async (
|
|
1148
|
+
options?: ServerReadOptions<T>,
|
|
1149
|
+
): Promise<CapturedRow<T>[]> => {
|
|
1150
|
+
const registry = readSetContext?.getStore();
|
|
1151
|
+
const rows = await load(options);
|
|
1152
|
+
if (!registry) return rows as CapturedRow<T>[];
|
|
1153
|
+
for (const row of rows) {
|
|
1154
|
+
const stamp = hydration.getReadEvidence?.(row as object);
|
|
1155
|
+
if (stamp === undefined) {
|
|
1156
|
+
// Local-first/lazy rows remain valid reads, but cannot later be used as
|
|
1157
|
+
// guarded dependencies. `prepareReadSet` rejects them if supplied in
|
|
1158
|
+
// `reads`; authoritative complete reads carry evidence here.
|
|
1159
|
+
continue;
|
|
1160
|
+
}
|
|
1161
|
+
const id = (row as { id?: unknown }).id;
|
|
1162
|
+
if (typeof id !== 'string' || id.length === 0) {
|
|
1163
|
+
throw new AbloValidationError(
|
|
1164
|
+
`${registeredModelName}.list returned a row without an id.`,
|
|
1165
|
+
{ code: 'commit_no_result' },
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
capturePointRead(
|
|
1169
|
+
readSetContext,
|
|
1170
|
+
readSetClientIdentity,
|
|
1171
|
+
wireModel,
|
|
1172
|
+
id,
|
|
1173
|
+
row,
|
|
1174
|
+
stamp,
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
return rows as CapturedRow<T>[];
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1090
1180
|
const operations: ModelOperations<T, C> = {
|
|
1091
1181
|
local,
|
|
1092
1182
|
|
|
@@ -1095,11 +1185,10 @@ export function createModelProxy<T, C>(
|
|
|
1095
1185
|
|
|
1096
1186
|
// No automatic scope enrolment on bulk `list`: that would subscribe to an
|
|
1097
1187
|
// unbounded set of rows' entity groups.
|
|
1098
|
-
list
|
|
1188
|
+
list,
|
|
1099
1189
|
|
|
1100
1190
|
create: guardWrite(async (params: ModelCreateParams<T, C>): Promise<T> => {
|
|
1101
1191
|
const id = params.id ?? Model.generateId();
|
|
1102
|
-
const opts = mutationOptions(params);
|
|
1103
1192
|
const claim = params.claim;
|
|
1104
1193
|
let autoLease: Claim | undefined;
|
|
1105
1194
|
if (claim && !isClaimHandle(claim)) {
|
|
@@ -1146,15 +1235,41 @@ export function createModelProxy<T, C>(
|
|
|
1146
1235
|
createdAt: new Date(),
|
|
1147
1236
|
updatedAt: new Date(),
|
|
1148
1237
|
});
|
|
1149
|
-
|
|
1150
|
-
...opts,
|
|
1151
|
-
...(autoLease ? { claim: autoLease } : {}),
|
|
1152
|
-
...(isClaimHandle(claim) ? { claim: { id: claim.id } } : {}),
|
|
1153
|
-
};
|
|
1238
|
+
let prepared: PreparedReadSet | undefined;
|
|
1154
1239
|
try {
|
|
1240
|
+
const resolved = preparedMutation(params);
|
|
1241
|
+
prepared = resolved.prepared;
|
|
1242
|
+
const effective: MutationOptions = {
|
|
1243
|
+
...resolved.options,
|
|
1244
|
+
...(autoLease
|
|
1245
|
+
? {
|
|
1246
|
+
claimRef: { id: autoLease.id },
|
|
1247
|
+
...(autoLease.fenceToken !== undefined
|
|
1248
|
+
? { fenceToken: autoLease.fenceToken }
|
|
1249
|
+
: {}),
|
|
1250
|
+
}
|
|
1251
|
+
: {}),
|
|
1252
|
+
...(isClaimHandle(claim)
|
|
1253
|
+
? {
|
|
1254
|
+
claimRef: { id: claim.id },
|
|
1255
|
+
...(claim.fenceToken !== undefined
|
|
1256
|
+
? { fenceToken: claim.fenceToken }
|
|
1257
|
+
: {}),
|
|
1258
|
+
}
|
|
1259
|
+
: {}),
|
|
1260
|
+
};
|
|
1155
1261
|
syncClient.add(model, effective);
|
|
1156
1262
|
await waitForMutation(model);
|
|
1263
|
+
consumeReadSet(
|
|
1264
|
+
readSetContext,
|
|
1265
|
+
readSetClientIdentity,
|
|
1266
|
+
prepared.consumed,
|
|
1267
|
+
prepared.automaticCommit,
|
|
1268
|
+
);
|
|
1157
1269
|
return modelAsRow<T>(model);
|
|
1270
|
+
} catch (error) {
|
|
1271
|
+
abortReadSetCommit(readSetContext, prepared?.automaticCommit ?? false);
|
|
1272
|
+
throw error;
|
|
1158
1273
|
} finally {
|
|
1159
1274
|
await autoLease?.release?.().catch(() => {});
|
|
1160
1275
|
}
|
|
@@ -1169,7 +1284,7 @@ export function createModelProxy<T, C>(
|
|
|
1169
1284
|
async (
|
|
1170
1285
|
arg: ModelUpdateParams<T, C> | string,
|
|
1171
1286
|
updater?: ModelUpdater<T>,
|
|
1172
|
-
contention?:
|
|
1287
|
+
contention?: FunctionalUpdateOptions<ReadDependency | CapturedRow>,
|
|
1173
1288
|
): Promise<T | undefined> => {
|
|
1174
1289
|
// Functional form: update(id, current => next). Same guarantee as the
|
|
1175
1290
|
// HTTP client (shared reconcile loop), implemented with this transport's
|
|
@@ -1194,18 +1309,29 @@ export function createModelProxy<T, C>(
|
|
|
1194
1309
|
{ code: 'model_claim_not_configured' },
|
|
1195
1310
|
);
|
|
1196
1311
|
}
|
|
1197
|
-
return reconcileFunctionalUpdate<
|
|
1312
|
+
return reconcileFunctionalUpdate<
|
|
1313
|
+
T,
|
|
1314
|
+
T,
|
|
1315
|
+
ReadDependency | CapturedRow
|
|
1316
|
+
>(updater, contention, {
|
|
1198
1317
|
model: registeredModelName,
|
|
1199
1318
|
id,
|
|
1200
1319
|
readFresh: async () => {
|
|
1201
1320
|
// `type: 'complete'` forces the round-trip — the hydration ledger
|
|
1202
1321
|
// would otherwise serve a possibly-stale local row for a hydrated id.
|
|
1203
1322
|
await load({ where: [['id', id]], type: 'complete' });
|
|
1204
|
-
const
|
|
1205
|
-
|
|
1323
|
+
const exact = await collaboration.readPoint(wireModel, id);
|
|
1324
|
+
capturePointRead(
|
|
1325
|
+
readSetContext,
|
|
1326
|
+
readSetClientIdentity,
|
|
1327
|
+
wireModel,
|
|
1328
|
+
id,
|
|
1329
|
+
exact.data,
|
|
1330
|
+
exact.stamp,
|
|
1331
|
+
);
|
|
1206
1332
|
return {
|
|
1207
|
-
data:
|
|
1208
|
-
stamp:
|
|
1333
|
+
data: exact.data as T | undefined,
|
|
1334
|
+
stamp: exact.stamp,
|
|
1209
1335
|
};
|
|
1210
1336
|
},
|
|
1211
1337
|
writeNext: async (patch, readAt) => {
|
|
@@ -1216,14 +1342,39 @@ export function createModelProxy<T, C>(
|
|
|
1216
1342
|
{ code: 'entity_not_found' },
|
|
1217
1343
|
);
|
|
1218
1344
|
}
|
|
1219
|
-
const
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1345
|
+
const prepared = prepareReadSet(
|
|
1346
|
+
readSetContext,
|
|
1347
|
+
readSetClientIdentity,
|
|
1348
|
+
undefined,
|
|
1349
|
+
'reject',
|
|
1350
|
+
undefined,
|
|
1351
|
+
contention?.reads,
|
|
1352
|
+
);
|
|
1353
|
+
try {
|
|
1354
|
+
const effective: MutationOptions = {
|
|
1355
|
+
readAt: prepared.readAt ?? readAt,
|
|
1356
|
+
onStale: prepared.onStale ?? 'reject',
|
|
1357
|
+
...(prepared.idempotencyKey
|
|
1358
|
+
? { idempotencyKey: prepared.idempotencyKey }
|
|
1359
|
+
: {}),
|
|
1360
|
+
...(prepared.reads !== undefined
|
|
1361
|
+
? { reads: prepared.reads === null ? null : [...prepared.reads] }
|
|
1362
|
+
: {}),
|
|
1363
|
+
};
|
|
1364
|
+
model.applyChanges(patch);
|
|
1365
|
+
syncClient.update(model, effective);
|
|
1366
|
+
await waitForMutation(model);
|
|
1367
|
+
consumeReadSet(
|
|
1368
|
+
readSetContext,
|
|
1369
|
+
readSetClientIdentity,
|
|
1370
|
+
prepared.consumed,
|
|
1371
|
+
prepared.automaticCommit,
|
|
1372
|
+
);
|
|
1373
|
+
return modelAsRow<T>(model);
|
|
1374
|
+
} catch (error) {
|
|
1375
|
+
abortReadSetCommit(readSetContext, prepared.automaticCommit);
|
|
1376
|
+
throw error;
|
|
1377
|
+
}
|
|
1227
1378
|
},
|
|
1228
1379
|
});
|
|
1229
1380
|
}
|
|
@@ -1256,13 +1407,17 @@ export function createModelProxy<T, C>(
|
|
|
1256
1407
|
// If we hold a claim on this row, guard the write with its snapshot
|
|
1257
1408
|
// watermark + lease so it's stale-rejected and attributed to the claim.
|
|
1258
1409
|
const claimed = activeClaims.get(id);
|
|
1259
|
-
const
|
|
1410
|
+
const resolved = preparedMutation(params);
|
|
1411
|
+
const opts = resolved.options;
|
|
1260
1412
|
const handle = isClaimHandle(params.claim) ? params.claim : undefined;
|
|
1261
1413
|
const effective: MutationOptions | undefined = claimed
|
|
1262
1414
|
? {
|
|
1263
1415
|
readAt: claimed.lease.readAt ?? claimed.snapshot.stamp,
|
|
1264
1416
|
onStale: 'reject',
|
|
1265
1417
|
claimRef: { id: claimed.lease.id },
|
|
1418
|
+
...(claimed.lease.fenceToken !== undefined
|
|
1419
|
+
? { fenceToken: claimed.lease.fenceToken }
|
|
1420
|
+
: {}),
|
|
1266
1421
|
...opts,
|
|
1267
1422
|
}
|
|
1268
1423
|
: {
|
|
@@ -1279,28 +1434,39 @@ export function createModelProxy<T, C>(
|
|
|
1279
1434
|
}
|
|
1280
1435
|
: {}),
|
|
1281
1436
|
...opts,
|
|
1282
|
-
...(handle ? {
|
|
1437
|
+
...(handle ? { claimRef: { id: handle.id } } : {}),
|
|
1283
1438
|
};
|
|
1284
1439
|
// Local user update: `applyChanges` keeps change tracking on so the
|
|
1285
1440
|
// edited fields land in `modifiedProperties` and are actually sent to
|
|
1286
1441
|
// the server. (`updateFromData` is the hydration path and would discard
|
|
1287
1442
|
// the tracking, producing an empty `input: {}` no-op mutation.)
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1443
|
+
try {
|
|
1444
|
+
model.applyChanges(params.data);
|
|
1445
|
+
syncClient.update(model, effective);
|
|
1446
|
+
await waitForMutation(model);
|
|
1447
|
+
consumeReadSet(
|
|
1448
|
+
readSetContext,
|
|
1449
|
+
readSetClientIdentity,
|
|
1450
|
+
resolved.prepared.consumed,
|
|
1451
|
+
resolved.prepared.automaticCommit,
|
|
1452
|
+
);
|
|
1453
|
+
return modelAsRow<T>(model);
|
|
1454
|
+
} catch (error) {
|
|
1455
|
+
abortReadSetCommit(readSetContext, resolved.prepared.automaticCommit);
|
|
1456
|
+
throw error;
|
|
1457
|
+
}
|
|
1292
1458
|
},
|
|
1293
1459
|
);
|
|
1294
1460
|
function update(params: ModelUpdateParams<T, C>): Promise<T>;
|
|
1295
1461
|
function update(
|
|
1296
1462
|
id: string,
|
|
1297
1463
|
updater: ModelUpdater<T>,
|
|
1298
|
-
options?:
|
|
1464
|
+
options?: FunctionalUpdateOptions<ReadDependency | CapturedRow>,
|
|
1299
1465
|
): Promise<T | undefined>;
|
|
1300
1466
|
function update(
|
|
1301
1467
|
arg: ModelUpdateParams<T, C> | string,
|
|
1302
1468
|
updater?: ModelUpdater<T>,
|
|
1303
|
-
contention?:
|
|
1469
|
+
contention?: FunctionalUpdateOptions<ReadDependency | CapturedRow>,
|
|
1304
1470
|
): Promise<T | undefined> {
|
|
1305
1471
|
return updateImpl(arg, updater, contention);
|
|
1306
1472
|
}
|
|
@@ -1338,7 +1504,8 @@ export function createModelProxy<T, C>(
|
|
|
1338
1504
|
// the same row).
|
|
1339
1505
|
if (!model) return;
|
|
1340
1506
|
const claimed = activeClaims.get(id);
|
|
1341
|
-
const
|
|
1507
|
+
const resolved = preparedMutation(params);
|
|
1508
|
+
const opts = resolved.options;
|
|
1342
1509
|
const handle = isClaimHandle(params.claim) ? params.claim : undefined;
|
|
1343
1510
|
const effective: MutationOptions | undefined = claimed
|
|
1344
1511
|
? {
|
|
@@ -1351,17 +1518,31 @@ export function createModelProxy<T, C>(
|
|
|
1351
1518
|
...opts,
|
|
1352
1519
|
}
|
|
1353
1520
|
: {
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1521
|
+
...(handle?.readAt !== undefined
|
|
1522
|
+
? {
|
|
1523
|
+
readAt: handle.readAt,
|
|
1524
|
+
onStale: 'reject' as const,
|
|
1525
|
+
...(handle.fenceToken !== undefined
|
|
1526
|
+
? { fenceToken: handle.fenceToken }
|
|
1527
|
+
: {}),
|
|
1528
|
+
}
|
|
1529
|
+
: {}),
|
|
1530
|
+
...opts,
|
|
1531
|
+
...(handle ? { claimRef: { id: handle.id } } : {}),
|
|
1362
1532
|
};
|
|
1363
|
-
|
|
1364
|
-
|
|
1533
|
+
try {
|
|
1534
|
+
syncClient.delete(model, effective);
|
|
1535
|
+
await waitForMutation(model);
|
|
1536
|
+
consumeReadSet(
|
|
1537
|
+
readSetContext,
|
|
1538
|
+
readSetClientIdentity,
|
|
1539
|
+
resolved.prepared.consumed,
|
|
1540
|
+
resolved.prepared.automaticCommit,
|
|
1541
|
+
);
|
|
1542
|
+
} catch (error) {
|
|
1543
|
+
abortReadSetCommit(readSetContext, resolved.prepared.automaticCommit);
|
|
1544
|
+
throw error;
|
|
1545
|
+
}
|
|
1365
1546
|
}),
|
|
1366
1547
|
|
|
1367
1548
|
// `claim` is a callable namespace (take a claim) carrying the coordination
|
|
@@ -20,7 +20,7 @@ import type {
|
|
|
20
20
|
DurableWriteStore,
|
|
21
21
|
DurableWritesConfig,
|
|
22
22
|
} from '@abloatai/transaction/durableWrites';
|
|
23
|
-
import type { CommitOutboxScope } from '@abloatai/transaction/transactions/
|
|
23
|
+
import type { CommitOutboxScope } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
|
|
24
24
|
|
|
25
25
|
// ── Options ───────────────────────────────────────────────────────────────
|
|
26
26
|
|
|
@@ -17,12 +17,13 @@ import { omittedModelError } from '@abloatai/transaction/schema/select';
|
|
|
17
17
|
import {
|
|
18
18
|
durableCommitOperationSchema,
|
|
19
19
|
type DurableCommitOperation,
|
|
20
|
-
} from '@abloatai/transaction/transactions/
|
|
20
|
+
} from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
|
|
21
21
|
import { AbloAuthenticationError, AbloConnectionError, AbloValidationError, claimedError } from '@abloatai/transaction/errors';
|
|
22
22
|
import type { ModelTarget, ModelClaim } from '@abloatai/transaction/coordination/schema';
|
|
23
23
|
import type { BatchFence } from '@abloatai/transaction/coordination';
|
|
24
24
|
import {
|
|
25
25
|
batchFence,
|
|
26
|
+
claimIdFor,
|
|
26
27
|
fenceTokenFor,
|
|
27
28
|
modelTarget,
|
|
28
29
|
streamTarget,
|
|
@@ -69,6 +70,14 @@ import {
|
|
|
69
70
|
import { createModelProxy, type ModelOperations } from './createModelProxy.js';
|
|
70
71
|
import { assertWriteOptions } from '@abloatai/transaction/resources/writeOptionsSchema';
|
|
71
72
|
import type { AbloClient as Ablo } from '../../client.js';
|
|
73
|
+
import {
|
|
74
|
+
modelReadResponseSchema,
|
|
75
|
+
commitRecordSchema,
|
|
76
|
+
commitRecordListSchema,
|
|
77
|
+
commitRecordWhereSchema,
|
|
78
|
+
} from '@abloatai/transaction/wire';
|
|
79
|
+
import { translateHttpError } from '@abloatai/transaction/errors';
|
|
80
|
+
import { kReadEvidence } from '@abloatai/transaction/internal/read-set';
|
|
72
81
|
|
|
73
82
|
/**
|
|
74
83
|
* What the reactive build is fed: the factory's pass over the options bag
|
|
@@ -123,12 +132,39 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
|
|
|
123
132
|
createSibling,
|
|
124
133
|
} = inputs;
|
|
125
134
|
const schema = options.schema;
|
|
135
|
+
const pointReadBaseUrl = resolveBootstrapBaseUrl({
|
|
136
|
+
url,
|
|
137
|
+
bootstrapBaseUrl: internalOptions.bootstrapBaseUrl,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
async function readPoint(model: string, id: string): Promise<{ data: unknown; stamp: number }> {
|
|
141
|
+
const fetchImpl = internalOptions.fetch ?? globalThis.fetch;
|
|
142
|
+
const response = await fetchImpl(
|
|
143
|
+
`${pointReadBaseUrl}/v1/models/${encodeURIComponent(model)}/${encodeURIComponent(id)}`,
|
|
144
|
+
{ headers: authCredentials.withAuthHeaders() },
|
|
145
|
+
);
|
|
146
|
+
let body: unknown;
|
|
147
|
+
try {
|
|
148
|
+
body = await response.json();
|
|
149
|
+
} catch {
|
|
150
|
+
body = null;
|
|
151
|
+
}
|
|
152
|
+
if (!response.ok) throw translateHttpError(response.status, body);
|
|
153
|
+
const parsed = modelReadResponseSchema.safeParse(body);
|
|
154
|
+
if (!parsed.success) {
|
|
155
|
+
throw new AbloConnectionError('Model point-read response failed validation.', {
|
|
156
|
+
code: 'commit_no_result',
|
|
157
|
+
cause: parsed.error,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return { data: parsed.data.data, stamp: parsed.data.stamp };
|
|
161
|
+
}
|
|
126
162
|
|
|
127
163
|
// The store cluster — this client's runtime, the component graph, the
|
|
128
164
|
// registered models, and the store — was constructed by `humans().init`
|
|
129
165
|
// from the widened plugin context (see `./storeCluster.ts`). The engine
|
|
130
166
|
// assembles around it.
|
|
131
|
-
const { components, store } = cluster;
|
|
167
|
+
const { components, store, readSetContext } = cluster;
|
|
132
168
|
const {
|
|
133
169
|
modelRegistry,
|
|
134
170
|
objectPool,
|
|
@@ -198,6 +234,7 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
|
|
|
198
234
|
/** Resolved account scope — seeded once identity resolution completes;
|
|
199
235
|
* exposed as the readonly `ablo.organizationId` accessor. */
|
|
200
236
|
let _resolvedOrganizationId: string | null = null;
|
|
237
|
+
let _resolvedIdentity: import('@abloatai/transaction/auth').EffectiveAuthority | null = null;
|
|
201
238
|
const lifecycle = startStoreLifecycle({
|
|
202
239
|
cluster,
|
|
203
240
|
schema,
|
|
@@ -210,10 +247,11 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
|
|
|
210
247
|
kind,
|
|
211
248
|
logger,
|
|
212
249
|
validationError: _validationError,
|
|
213
|
-
onIdentityResolved: ({ userId, participantKind, accountScope, syncGroups }) => {
|
|
250
|
+
onIdentityResolved: ({ userId, participantKind, accountScope, syncGroups, authority }) => {
|
|
214
251
|
selfParticipantId = userId;
|
|
215
252
|
selfParticipantKind = participantKind;
|
|
216
253
|
_resolvedOrganizationId = accountScope;
|
|
254
|
+
_resolvedIdentity = authority;
|
|
217
255
|
presenceStream.setParticipant({
|
|
218
256
|
id: userId,
|
|
219
257
|
kind: participantKind,
|
|
@@ -260,6 +298,7 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
|
|
|
260
298
|
op: CommitOperationInput,
|
|
261
299
|
defaults: Pick<CommitCreateOptions, 'readAt' | 'onStale'>,
|
|
262
300
|
fence: BatchFence | null,
|
|
301
|
+
claim: Claim | null,
|
|
263
302
|
): DurableCommitOperation {
|
|
264
303
|
const type = op.action.toUpperCase();
|
|
265
304
|
const id = op.id ?? '';
|
|
@@ -269,6 +308,8 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
|
|
|
269
308
|
id,
|
|
270
309
|
input: op.data ?? undefined,
|
|
271
310
|
transactionId: op.transactionId ?? undefined,
|
|
311
|
+
claimId:
|
|
312
|
+
op.claimId ?? claimIdFor(claim?.target, claim?.id, op.model, op.id ?? null) ?? undefined,
|
|
272
313
|
readAt: op.readAt ?? defaults.readAt ?? undefined,
|
|
273
314
|
onStale: op.onStale ?? defaults.onStale ?? undefined,
|
|
274
315
|
fenceToken:
|
|
@@ -287,7 +328,7 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
|
|
|
287
328
|
);
|
|
288
329
|
}
|
|
289
330
|
return commitOptions.operations.map((op) =>
|
|
290
|
-
normalizeCommitOperation(op, commitOptions, fence),
|
|
331
|
+
normalizeCommitOperation(op, commitOptions, fence, commitOptions.claim ?? null),
|
|
291
332
|
);
|
|
292
333
|
}
|
|
293
334
|
|
|
@@ -548,6 +589,7 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
|
|
|
548
589
|
hydration,
|
|
549
590
|
{
|
|
550
591
|
createClaim: (claimOptions) => publicClaims.create(claimOptions),
|
|
592
|
+
readPoint,
|
|
551
593
|
createSnapshot: (modelKey, id) =>
|
|
552
594
|
createSnapshot({
|
|
553
595
|
pool: objectPool,
|
|
@@ -607,6 +649,7 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
|
|
|
607
649
|
...(options?.ttl !== undefined ? { ttl: options.ttl } : {}),
|
|
608
650
|
}),
|
|
609
651
|
},
|
|
652
|
+
readSetContext,
|
|
610
653
|
);
|
|
611
654
|
}
|
|
612
655
|
|
|
@@ -668,6 +711,31 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
|
|
|
668
711
|
...(missingIds && missingIds.length > 0 ? { missingIds } : {}),
|
|
669
712
|
};
|
|
670
713
|
},
|
|
714
|
+
async get({ id }) {
|
|
715
|
+
await ready();
|
|
716
|
+
const response = await (internalOptions.fetch ?? globalThis.fetch)(
|
|
717
|
+
`${pointReadBaseUrl}/v1/commits/${encodeURIComponent(id)}`,
|
|
718
|
+
{ headers: authCredentials.withAuthHeaders() },
|
|
719
|
+
);
|
|
720
|
+
const body: unknown = await response.json().catch(() => null);
|
|
721
|
+
if (!response.ok) throw translateHttpError(response.status, body);
|
|
722
|
+
return commitRecordSchema.nullable().parse(body);
|
|
723
|
+
},
|
|
724
|
+
async list(listOptions = {}) {
|
|
725
|
+
await ready();
|
|
726
|
+
const where = commitRecordWhereSchema.parse(listOptions.where ?? {});
|
|
727
|
+
const params = new URLSearchParams();
|
|
728
|
+
if (where.actorId) params.set('actorId', where.actorId);
|
|
729
|
+
if (where.status) params.set('status', where.status);
|
|
730
|
+
const query = params.size > 0 ? `?${params.toString()}` : '';
|
|
731
|
+
const response = await (internalOptions.fetch ?? globalThis.fetch)(
|
|
732
|
+
`${pointReadBaseUrl}/v1/commits${query}`,
|
|
733
|
+
{ headers: authCredentials.withAuthHeaders() },
|
|
734
|
+
);
|
|
735
|
+
const body: unknown = await response.json().catch(() => null);
|
|
736
|
+
if (!response.ok) throw translateHttpError(response.status, body);
|
|
737
|
+
return commitRecordListSchema.parse(body);
|
|
738
|
+
},
|
|
671
739
|
};
|
|
672
740
|
|
|
673
741
|
/**
|
|
@@ -767,6 +835,10 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
|
|
|
767
835
|
return _resolvedOrganizationId;
|
|
768
836
|
},
|
|
769
837
|
|
|
838
|
+
get identity() {
|
|
839
|
+
return _resolvedIdentity;
|
|
840
|
+
},
|
|
841
|
+
|
|
770
842
|
nudgeReconnect() {
|
|
771
843
|
store.nudgeReconnect();
|
|
772
844
|
},
|
|
@@ -955,6 +1027,11 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
|
|
|
955
1027
|
},
|
|
956
1028
|
} as Ablo<S>;
|
|
957
1029
|
|
|
1030
|
+
Object.defineProperty(engine, kReadEvidence, {
|
|
1031
|
+
value: { context: cluster.readSetContext, client: syncClient as object },
|
|
1032
|
+
enumerable: false,
|
|
1033
|
+
});
|
|
1034
|
+
|
|
958
1035
|
// A model the schema projection left out answers with an error naming the
|
|
959
1036
|
// model and the fix, not `undefined`. An app can compile against the full
|
|
960
1037
|
// source schema while running a projection, so the type system never sees
|
|
@@ -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
|