@abloatai/ablo 0.10.1 → 0.11.1
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 +34 -0
- package/README.md +63 -23
- package/dist/BaseSyncedStore.d.ts +75 -0
- package/dist/BaseSyncedStore.js +193 -8
- package/dist/Database.d.ts +10 -2
- package/dist/Database.js +15 -1
- package/dist/SyncClient.d.ts +12 -1
- package/dist/SyncClient.js +110 -26
- package/dist/agent/Agent.d.ts +9 -9
- package/dist/agent/Agent.js +16 -16
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/index.js +2 -2
- package/dist/agent/types.d.ts +1 -1
- package/dist/agent/types.js +1 -1
- package/dist/ai-sdk/{intent-broadcast.d.ts → claim-broadcast.d.ts} +10 -10
- package/dist/ai-sdk/{intent-broadcast.js → claim-broadcast.js} +6 -6
- package/dist/ai-sdk/coordination-context.d.ts +9 -9
- package/dist/ai-sdk/coordination-context.js +8 -8
- package/dist/ai-sdk/index.d.ts +1 -1
- package/dist/ai-sdk/index.js +1 -1
- package/dist/ai-sdk/wrap.d.ts +4 -4
- package/dist/ai-sdk/wrap.js +4 -4
- package/dist/api/index.d.ts +2 -2
- package/dist/cli.cjs +369 -67
- package/dist/client/Ablo.d.ts +30 -63
- package/dist/client/Ablo.js +124 -103
- package/dist/client/ApiClient.d.ts +6 -5
- package/dist/client/ApiClient.js +86 -62
- package/dist/client/auth.d.ts +9 -4
- package/dist/client/auth.js +40 -5
- package/dist/client/createModelProxy.d.ts +41 -54
- package/dist/client/createModelProxy.js +123 -20
- package/dist/client/httpClient.d.ts +2 -0
- package/dist/client/httpClient.js +1 -1
- package/dist/client/index.d.ts +3 -3
- package/dist/client/writeOptionsSchema.d.ts +4 -4
- package/dist/client/writeOptionsSchema.js +4 -4
- package/dist/coordination/schema.d.ts +249 -38
- package/dist/coordination/schema.js +172 -39
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +4 -4
- package/dist/errorCodes.d.ts +9 -9
- package/dist/errorCodes.js +16 -16
- package/dist/errors.d.ts +51 -2
- package/dist/errors.js +94 -5
- package/dist/interfaces/index.d.ts +8 -4
- package/dist/policy/index.d.ts +1 -1
- package/dist/policy/types.d.ts +13 -13
- package/dist/policy/types.js +8 -8
- package/dist/react/AbloProvider.d.ts +51 -4
- package/dist/react/AbloProvider.js +95 -11
- package/dist/react/context.d.ts +26 -9
- package/dist/react/context.js +2 -2
- package/dist/react/index.d.ts +4 -4
- package/dist/react/index.js +4 -4
- package/dist/react/useAblo.js +5 -5
- package/dist/react/{useIntent.d.ts → useClaim.d.ts} +9 -9
- package/dist/react/useClaim.js +42 -0
- package/dist/schema/index.js +1 -1
- package/dist/schema/schema.d.ts +3 -3
- package/dist/schema/sugar.d.ts +3 -3
- package/dist/schema/sugar.js +3 -3
- package/dist/schema/sync-delta-wire.d.ts +8 -8
- package/dist/server/commit.d.ts +2 -2
- package/dist/sync/AreaOfInterestManager.d.ts +162 -0
- package/dist/sync/AreaOfInterestManager.js +233 -0
- package/dist/sync/BootstrapHelper.d.ts +9 -1
- package/dist/sync/BootstrapHelper.js +15 -5
- package/dist/sync/NetworkProbe.d.ts +1 -1
- package/dist/sync/NetworkProbe.js +1 -1
- package/dist/sync/SyncWebSocket.d.ts +59 -25
- package/dist/sync/SyncWebSocket.js +123 -26
- package/dist/sync/awaitClaimGrant.d.ts +40 -0
- package/dist/sync/awaitClaimGrant.js +86 -0
- package/dist/sync/createClaimStream.d.ts +34 -0
- package/dist/sync/{createIntentStream.js → createClaimStream.js} +92 -81
- package/dist/sync/createPresenceStream.js +3 -2
- package/dist/sync/participants.d.ts +10 -10
- package/dist/sync/participants.js +17 -10
- package/dist/sync/schemas.d.ts +8 -8
- package/dist/transactions/TransactionQueue.d.ts +23 -0
- package/dist/transactions/TransactionQueue.js +186 -12
- package/dist/types/global.d.ts +18 -13
- package/dist/types/global.js +11 -6
- package/dist/types/index.d.ts +9 -7
- package/dist/types/index.js +2 -2
- package/dist/types/streams.d.ts +114 -98
- package/dist/types/streams.js +1 -1
- package/dist/utils/asyncIterator.d.ts +1 -1
- package/dist/utils/asyncIterator.js +1 -1
- package/dist/wire/frames.d.ts +2 -2
- package/docs/api.md +3 -3
- package/docs/client-behavior.md +6 -3
- package/docs/coordination.md +13 -3
- package/docs/data-sources.md +29 -9
- package/docs/migration.md +40 -0
- package/docs/quickstart.md +61 -33
- package/docs/react.md +46 -0
- package/llms-full.txt +25 -8
- package/llms.txt +11 -9
- package/package.json +3 -2
- package/dist/react/useIntent.js +0 -42
- package/dist/sync/awaitIntentGrant.d.ts +0 -40
- package/dist/sync/awaitIntentGrant.js +0 -62
- package/dist/sync/createIntentStream.d.ts +0 -34
package/dist/client/Ablo.js
CHANGED
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
* await sync.reports.delete({ id: reportId });
|
|
20
20
|
*/
|
|
21
21
|
import { z } from 'zod';
|
|
22
|
-
import {
|
|
22
|
+
import { baseFieldsSchema } from '../schema/schema.js';
|
|
23
|
+
import { AbloError, AbloAuthenticationError, AbloConnectionError, AbloValidationError, translateHttpError, toAbloError, claimedError } from '../errors.js';
|
|
24
|
+
import { descriptionFromMeta } from '../coordination/schema.js';
|
|
23
25
|
import { LoadStrategy, PropertyType } from '../types/index.js';
|
|
24
26
|
import { initSyncEngine } from '../context.js';
|
|
25
27
|
import { noopObservability, browserOnlineStatus, defaultSessionErrorDetector, noopAnalytics, } from '../SyncEngineContext.js';
|
|
@@ -32,15 +34,15 @@ import { resolveParticipantIdentity } from './identity.js';
|
|
|
32
34
|
import { Model } from '../Model.js';
|
|
33
35
|
import { BaseSyncedStore } from '../BaseSyncedStore.js';
|
|
34
36
|
import { createPresenceStream } from '../sync/createPresenceStream.js';
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
37
|
+
import { createClaimStream } from '../sync/createClaimStream.js';
|
|
38
|
+
import { awaitClaimGrant } from '../sync/awaitClaimGrant.js';
|
|
37
39
|
import { createSnapshot } from '../sync/createSnapshot.js';
|
|
38
40
|
import { createParticipantManager } from '../sync/participants.js';
|
|
39
41
|
import { createProtocolClient, } from './ApiClient.js';
|
|
40
42
|
// Value import is cycle-safe: httpClient.js only value-imports ApiClient.js,
|
|
41
43
|
// which imports this module type-only.
|
|
42
44
|
import { createAbloHttpClient, } from './httpClient.js';
|
|
43
|
-
import { assertBrowserSafety, readProcessEnv, resolveApiKey, resolveApiKeyValue, resolveAuthToken, resolveBaseURL, resolveBootstrapBaseUrl, resolveDatabaseUrl, } from './auth.js';
|
|
45
|
+
import { assertBrowserSafety, readProcessEnv, resolveApiKey, resolveApiKeyValue, resolveAuthToken, resolveBaseURL, resolveBootstrapBaseUrl, resolveDatabaseUrl, warnIfDatabaseUrlEnvIgnored, } from './auth.js';
|
|
44
46
|
import { registerDataSource } from './registerDataSource.js';
|
|
45
47
|
import { shouldUseInMemoryPersistence, } from './persistence.js';
|
|
46
48
|
import { createModelProxy } from './createModelProxy.js';
|
|
@@ -240,7 +242,18 @@ function registerModelsFromSchema(schema, registry) {
|
|
|
240
242
|
}
|
|
241
243
|
// Create a dynamic Model subclass with JSON sub-property getters
|
|
242
244
|
const isLazy = modelDef.lazyObservable === true;
|
|
243
|
-
|
|
245
|
+
// Base provenance fields (`organizationId`, `createdBy`) live in
|
|
246
|
+
// `baseFieldsSchema`, not the per-model `shape`. The server stamps + emits
|
|
247
|
+
// them (camelCased on the wire), but hydration (`Model.assignFieldsFromData`)
|
|
248
|
+
// only assigns keys that already exist as an own/prototype property — so
|
|
249
|
+
// without a slot here, `deck.createdBy` / `deck.organizationId` silently read
|
|
250
|
+
// `undefined` (this is why the profile decks tab showed nothing: it filters
|
|
251
|
+
// `decks.filter(d => d.createdBy === userId)`). `id`/`createdAt`/`updatedAt`
|
|
252
|
+
// are already seeded by the base Model constructor, so they're excluded.
|
|
253
|
+
const fieldNames = [
|
|
254
|
+
...Object.keys(modelDef.shape),
|
|
255
|
+
...Object.keys(baseFieldsSchema.shape).filter((f) => f !== 'id' && f !== 'createdAt' && f !== 'updatedAt' && !(f in modelDef.shape)),
|
|
256
|
+
];
|
|
244
257
|
const computed = modelDef.computed;
|
|
245
258
|
const DynamicModel = createDynamicModelClass(modelName, jsonSubFields, fieldNames, computed, isLazy);
|
|
246
259
|
// Respect the schema's load strategy so lazy models skip IDB hydration + bootstrap
|
|
@@ -712,6 +725,9 @@ export function Ablo(options) {
|
|
|
712
725
|
dangerouslyAllowBrowser: options.dangerouslyAllowBrowser,
|
|
713
726
|
});
|
|
714
727
|
const { logger = consoleLogger } = internalOptions;
|
|
728
|
+
// Nudge (once) if a stray DATABASE_URL is in the env but `databaseUrl` wasn't
|
|
729
|
+
// passed — the env value is no longer auto-adopted (see resolveDatabaseUrl).
|
|
730
|
+
warnIfDatabaseUrlEnvIgnored(authInput, (m) => logger.warn(m));
|
|
715
731
|
const schema = options.schema;
|
|
716
732
|
const url = resolveBaseURL(authInput);
|
|
717
733
|
// 1. Derive config from schema
|
|
@@ -821,8 +837,8 @@ export function Ablo(options) {
|
|
|
821
837
|
// becomes null, so the first Ablo's commits start throwing
|
|
822
838
|
// `ws_not_ready` forever (terminal AgentJob writes hang on retry).
|
|
823
839
|
syncClient.getTransactionQueue().setMutationExecutor(executor);
|
|
824
|
-
// Presence +
|
|
825
|
-
// and `engine.
|
|
840
|
+
// Presence + claim streams — built eagerly so `engine.presence`
|
|
841
|
+
// and `engine.claims` return the same reference for the engine's
|
|
826
842
|
// lifetime. The transport doesn't exist yet (BaseSyncedStore.initialize
|
|
827
843
|
// creates it during ready()), so both streams are constructed in
|
|
828
844
|
// deferred-attach mode and wired after initialize() resolves below.
|
|
@@ -837,12 +853,12 @@ export function Ablo(options) {
|
|
|
837
853
|
syncGroups: internalOptions.syncGroups ?? [],
|
|
838
854
|
isAgent: internalOptions.kind === 'agent',
|
|
839
855
|
});
|
|
840
|
-
const
|
|
856
|
+
const claimStream = createClaimStream({ participantId });
|
|
841
857
|
const participantManager = createParticipantManager({
|
|
842
858
|
ready,
|
|
843
859
|
getTransport: () => store.getSyncWebSocket() ?? null,
|
|
844
860
|
presence: presenceStream,
|
|
845
|
-
|
|
861
|
+
claims: claimStream,
|
|
846
862
|
schema,
|
|
847
863
|
});
|
|
848
864
|
// 6. Validate options up front — fail loudly on obviously wrong inputs so
|
|
@@ -990,14 +1006,14 @@ export function Ablo(options) {
|
|
|
990
1006
|
code: 'bootstrap_fetch_timeout',
|
|
991
1007
|
});
|
|
992
1008
|
}
|
|
993
|
-
// Wire presence +
|
|
1009
|
+
// Wire presence + claims to the now-open transport.
|
|
994
1010
|
// `getSyncWebSocket()` returns non-null after a successful
|
|
995
1011
|
// initialize() — the WS is created during the generator's
|
|
996
1012
|
// connect step.
|
|
997
1013
|
const ws = store.getSyncWebSocket();
|
|
998
1014
|
if (ws) {
|
|
999
1015
|
presenceStream.attach(ws);
|
|
1000
|
-
|
|
1016
|
+
claimStream.attach(ws);
|
|
1001
1017
|
}
|
|
1002
1018
|
logger.info('Sync engine ready', { models: Object.keys(schema.models).length });
|
|
1003
1019
|
}
|
|
@@ -1073,10 +1089,10 @@ export function Ablo(options) {
|
|
|
1073
1089
|
? crypto.randomUUID()
|
|
1074
1090
|
: `id_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
1075
1091
|
}
|
|
1076
|
-
function
|
|
1077
|
-
if (typeof
|
|
1078
|
-
return
|
|
1079
|
-
return
|
|
1092
|
+
function normalizeClaimId(claim) {
|
|
1093
|
+
if (typeof claim === 'string')
|
|
1094
|
+
return claim;
|
|
1095
|
+
return claim?.id;
|
|
1080
1096
|
}
|
|
1081
1097
|
function isClaimHandleValue(value) {
|
|
1082
1098
|
return (typeof value === 'object' &&
|
|
@@ -1113,82 +1129,72 @@ export function Ablo(options) {
|
|
|
1113
1129
|
}
|
|
1114
1130
|
return inputOperations.map((op) => normalizeCommitOperation(op, commitOptions));
|
|
1115
1131
|
}
|
|
1116
|
-
function modelClaimFromActive(
|
|
1117
|
-
const description =
|
|
1118
|
-
? intent.target.meta.description
|
|
1119
|
-
: undefined;
|
|
1132
|
+
function modelClaimFromActive(claim) {
|
|
1133
|
+
const description = descriptionFromMeta(claim.target.meta);
|
|
1120
1134
|
return {
|
|
1121
|
-
id:
|
|
1122
|
-
actor:
|
|
1123
|
-
participantKind:
|
|
1124
|
-
action:
|
|
1135
|
+
id: claim.id,
|
|
1136
|
+
actor: claim.heldBy,
|
|
1137
|
+
participantKind: claim.participantKind,
|
|
1138
|
+
action: claim.reason,
|
|
1125
1139
|
...(description ? { description } : {}),
|
|
1126
|
-
field:
|
|
1140
|
+
field: claim.target.field,
|
|
1127
1141
|
status: 'active',
|
|
1128
|
-
expiresAt:
|
|
1142
|
+
expiresAt: claim.expiresAt,
|
|
1129
1143
|
target: {
|
|
1130
|
-
model:
|
|
1131
|
-
id:
|
|
1132
|
-
path:
|
|
1133
|
-
range:
|
|
1134
|
-
field:
|
|
1135
|
-
meta:
|
|
1144
|
+
model: claim.target.type,
|
|
1145
|
+
id: claim.target.id,
|
|
1146
|
+
path: claim.target.path,
|
|
1147
|
+
range: claim.target.range,
|
|
1148
|
+
field: claim.target.field,
|
|
1149
|
+
meta: claim.target.meta,
|
|
1136
1150
|
},
|
|
1137
1151
|
};
|
|
1138
1152
|
}
|
|
1139
|
-
function modelClaimFromQueued(
|
|
1153
|
+
function modelClaimFromQueued(claim) {
|
|
1140
1154
|
return {
|
|
1141
|
-
id:
|
|
1142
|
-
actor:
|
|
1143
|
-
participantKind:
|
|
1144
|
-
action:
|
|
1145
|
-
...(
|
|
1146
|
-
field:
|
|
1155
|
+
id: claim.id,
|
|
1156
|
+
actor: claim.heldBy,
|
|
1157
|
+
participantKind: claim.participantKind,
|
|
1158
|
+
action: claim.action,
|
|
1159
|
+
...(claim.description ? { description: claim.description } : {}),
|
|
1160
|
+
field: claim.target.field,
|
|
1147
1161
|
status: 'queued',
|
|
1148
|
-
position:
|
|
1149
|
-
expiresAt:
|
|
1162
|
+
position: claim.position,
|
|
1163
|
+
expiresAt: claim.expiresAt,
|
|
1150
1164
|
target: {
|
|
1151
|
-
model:
|
|
1152
|
-
id:
|
|
1153
|
-
path:
|
|
1154
|
-
range:
|
|
1155
|
-
field:
|
|
1156
|
-
meta:
|
|
1165
|
+
model: claim.target.type,
|
|
1166
|
+
id: claim.target.id,
|
|
1167
|
+
path: claim.target.path,
|
|
1168
|
+
range: claim.target.range,
|
|
1169
|
+
field: claim.target.field,
|
|
1170
|
+
meta: claim.target.meta,
|
|
1157
1171
|
},
|
|
1158
1172
|
};
|
|
1159
1173
|
}
|
|
1160
|
-
function targetMatchesModel(target,
|
|
1174
|
+
function targetMatchesModel(target, claim) {
|
|
1161
1175
|
if (target.model &&
|
|
1162
|
-
|
|
1176
|
+
claim.target.type.toLowerCase() !== target.model.toLowerCase()) {
|
|
1163
1177
|
return false;
|
|
1164
1178
|
}
|
|
1165
|
-
if (target.id &&
|
|
1179
|
+
if (target.id && claim.target.id !== target.id)
|
|
1166
1180
|
return false;
|
|
1167
|
-
if (target.field &&
|
|
1181
|
+
if (target.field && claim.target.field !== target.field)
|
|
1168
1182
|
return false;
|
|
1169
1183
|
return true;
|
|
1170
1184
|
}
|
|
1171
1185
|
function listModelClaims(target) {
|
|
1172
|
-
return
|
|
1173
|
-
.filter((
|
|
1186
|
+
return claimStream.others
|
|
1187
|
+
.filter((claim) => (target ? targetMatchesModel(target, claim) : true))
|
|
1174
1188
|
.map(modelClaimFromActive);
|
|
1175
1189
|
}
|
|
1176
1190
|
function listModelClaimQueue(target) {
|
|
1177
1191
|
if (!target?.model || !target.id)
|
|
1178
1192
|
return [];
|
|
1179
|
-
return
|
|
1193
|
+
return publicClaims
|
|
1180
1194
|
.queueFor({ type: target.model, id: target.id })
|
|
1181
|
-
.filter((
|
|
1195
|
+
.filter((claim) => (target.field ? claim.target.field === target.field : true))
|
|
1182
1196
|
.map(modelClaimFromQueued);
|
|
1183
1197
|
}
|
|
1184
|
-
function claimedError(target, claims, code) {
|
|
1185
|
-
const label = [target.model, target.id, target.field].filter(Boolean).join('/');
|
|
1186
|
-
const holder = claims[0];
|
|
1187
|
-
const suffix = holder
|
|
1188
|
-
? ` held by ${holder.actor} (${holder.action})`
|
|
1189
|
-
: ' held by another participant';
|
|
1190
|
-
return new AbloClaimedError(`Model row is claimed: ${label || 'target'}${suffix}.`, { code, claims });
|
|
1191
|
-
}
|
|
1192
1198
|
function waitForModelUnclaimed(target, options) {
|
|
1193
1199
|
if (listModelClaims(target).length === 0)
|
|
1194
1200
|
return Promise.resolve();
|
|
@@ -1216,8 +1222,8 @@ export function Ablo(options) {
|
|
|
1216
1222
|
}
|
|
1217
1223
|
};
|
|
1218
1224
|
const onAbort = () => {
|
|
1219
|
-
finish(() => reject(new AbloConnectionError('
|
|
1220
|
-
code: '
|
|
1225
|
+
finish(() => reject(new AbloConnectionError('Claim wait aborted.', {
|
|
1226
|
+
code: 'claim_wait_aborted',
|
|
1221
1227
|
cause: options?.signal?.reason,
|
|
1222
1228
|
})));
|
|
1223
1229
|
};
|
|
@@ -1225,7 +1231,7 @@ export function Ablo(options) {
|
|
|
1225
1231
|
onAbort();
|
|
1226
1232
|
return;
|
|
1227
1233
|
}
|
|
1228
|
-
unsubscribe =
|
|
1234
|
+
unsubscribe = claimStream.onChange(check);
|
|
1229
1235
|
options?.signal?.addEventListener('abort', onAbort, { once: true });
|
|
1230
1236
|
if (options?.timeout != null) {
|
|
1231
1237
|
timeoutId = setTimeout(() => {
|
|
@@ -1250,58 +1256,61 @@ export function Ablo(options) {
|
|
|
1250
1256
|
}
|
|
1251
1257
|
await waitForModelUnclaimed(target, { timeout: options?.claimedTimeout });
|
|
1252
1258
|
}
|
|
1253
|
-
function
|
|
1259
|
+
function wrapClaimHandle(claim, waited = false) {
|
|
1254
1260
|
const release = async () => {
|
|
1255
1261
|
claim.revoke();
|
|
1256
1262
|
};
|
|
1257
1263
|
return {
|
|
1258
|
-
|
|
1264
|
+
object: 'claim',
|
|
1265
|
+
claimId: claim.claimId,
|
|
1266
|
+
action: claim.action,
|
|
1267
|
+
target: claim.target,
|
|
1259
1268
|
waited,
|
|
1260
1269
|
release,
|
|
1261
1270
|
revoke: claim.revoke,
|
|
1262
1271
|
[Symbol.asyncDispose]: release,
|
|
1263
1272
|
};
|
|
1264
1273
|
}
|
|
1265
|
-
const
|
|
1266
|
-
async create(
|
|
1274
|
+
const publicClaims = Object.assign(claimStream, {
|
|
1275
|
+
async create(claimOptions) {
|
|
1267
1276
|
await ready();
|
|
1268
|
-
const claim =
|
|
1269
|
-
type:
|
|
1270
|
-
id:
|
|
1271
|
-
path:
|
|
1272
|
-
range:
|
|
1273
|
-
field:
|
|
1274
|
-
meta:
|
|
1277
|
+
const claim = claimStream.claim({
|
|
1278
|
+
type: claimOptions.target.model,
|
|
1279
|
+
id: claimOptions.target.id,
|
|
1280
|
+
path: claimOptions.target.path,
|
|
1281
|
+
range: claimOptions.target.range,
|
|
1282
|
+
field: claimOptions.target.field,
|
|
1283
|
+
meta: claimOptions.target.meta,
|
|
1275
1284
|
}, {
|
|
1276
|
-
reason:
|
|
1277
|
-
ttl:
|
|
1278
|
-
queue:
|
|
1285
|
+
reason: claimOptions.action,
|
|
1286
|
+
ttl: claimOptions.ttl,
|
|
1287
|
+
queue: claimOptions.queue,
|
|
1279
1288
|
});
|
|
1280
1289
|
// With `queue`, the claim is only really *ours* once the server says
|
|
1281
|
-
// so (`
|
|
1290
|
+
// so (`claim_acquired` if the target was free, `claim_granted` once
|
|
1282
1291
|
// we reach the head of the FIFO line). Block here on that grant so
|
|
1283
1292
|
// callers — chiefly `ablo.<model>.claim` — get a handle that already
|
|
1284
1293
|
// holds the lease, never a half-claimed one racing the queue.
|
|
1285
1294
|
let waited = false;
|
|
1286
|
-
if (
|
|
1295
|
+
if (claimOptions.queue) {
|
|
1287
1296
|
const ws = store.getSyncWebSocket();
|
|
1288
1297
|
if (ws) {
|
|
1289
1298
|
try {
|
|
1290
|
-
({ waited } = await
|
|
1291
|
-
timeoutMs:
|
|
1292
|
-
maxQueueDepth:
|
|
1299
|
+
({ waited } = await awaitClaimGrant(ws, claim.claimId, {
|
|
1300
|
+
timeoutMs: claimOptions.waitTimeoutMs,
|
|
1301
|
+
maxQueueDepth: claimOptions.maxQueueDepth,
|
|
1293
1302
|
}));
|
|
1294
1303
|
}
|
|
1295
1304
|
catch (err) {
|
|
1296
1305
|
// Gave up waiting (queue too deep, timed out, or lost) — abandon
|
|
1297
|
-
// the queued
|
|
1306
|
+
// the queued claim so we don't leave a phantom entry in the
|
|
1298
1307
|
// line that would block or mislead other claimers.
|
|
1299
1308
|
claim.revoke();
|
|
1300
1309
|
throw err;
|
|
1301
1310
|
}
|
|
1302
1311
|
}
|
|
1303
1312
|
}
|
|
1304
|
-
return
|
|
1313
|
+
return wrapClaimHandle(claim, waited);
|
|
1305
1314
|
},
|
|
1306
1315
|
list(target) {
|
|
1307
1316
|
return listModelClaims(target);
|
|
@@ -1310,14 +1319,14 @@ export function Ablo(options) {
|
|
|
1310
1319
|
return waitForModelUnclaimed(target, options);
|
|
1311
1320
|
},
|
|
1312
1321
|
});
|
|
1313
|
-
// Build the typed proxy — one property per model. Done after
|
|
1322
|
+
// Build the typed proxy — one property per model. Done after publicClaims
|
|
1314
1323
|
// exists so model clients can expose workflow helpers such as
|
|
1315
1324
|
// `ablo.files.edit(...)` without importing protocol wiring.
|
|
1316
1325
|
const modelProxies = {};
|
|
1317
1326
|
for (const [schemaKey, modelDef] of Object.entries(schema.models)) {
|
|
1318
1327
|
const registeredModelName = modelDef.typename ?? schemaKey;
|
|
1319
1328
|
modelProxies[schemaKey] = createModelProxy(schemaKey, registeredModelName, objectPool, syncClient, modelRegistry, hydration, {
|
|
1320
|
-
|
|
1329
|
+
createClaim: (claimOptions) => publicClaims.create(claimOptions),
|
|
1321
1330
|
createSnapshot: (modelKey, id) => createSnapshot({
|
|
1322
1331
|
pool: objectPool,
|
|
1323
1332
|
transport: store.getSyncWebSocket(),
|
|
@@ -1331,21 +1340,21 @@ export function Ablo(options) {
|
|
|
1331
1340
|
getLastSyncId: () => syncClient.position.readFloor,
|
|
1332
1341
|
entities: { [modelKey]: id },
|
|
1333
1342
|
}),
|
|
1334
|
-
queue: (target) =>
|
|
1335
|
-
reorder: (target, order) =>
|
|
1343
|
+
queue: (target) => publicClaims.queueFor({ type: target.model, id: target.id }),
|
|
1344
|
+
reorder: (target, order) => publicClaims.reorder({ type: target.model, id: target.id }, order),
|
|
1336
1345
|
observe: (target) => {
|
|
1337
|
-
// The live
|
|
1346
|
+
// The live claim stream only tracks *open* (active) claims;
|
|
1338
1347
|
// terminal states (committed / expired / canceled) drop out of
|
|
1339
1348
|
// the list entirely — exactly the ephemeral coordination model.
|
|
1340
1349
|
// So a present entry is, by definition, `status: 'active'`.
|
|
1341
|
-
const held =
|
|
1350
|
+
const held = publicClaims.list({
|
|
1342
1351
|
model: target.model,
|
|
1343
1352
|
id: target.id,
|
|
1344
1353
|
})[0];
|
|
1345
1354
|
if (!held)
|
|
1346
1355
|
return null;
|
|
1347
1356
|
return {
|
|
1348
|
-
object: '
|
|
1357
|
+
object: 'claim',
|
|
1349
1358
|
id: held.id,
|
|
1350
1359
|
status: 'active',
|
|
1351
1360
|
target: {
|
|
@@ -1362,8 +1371,20 @@ export function Ablo(options) {
|
|
|
1362
1371
|
expiresAt: held.expiresAt,
|
|
1363
1372
|
};
|
|
1364
1373
|
},
|
|
1365
|
-
waitFor: (target, waitOptions) =>
|
|
1374
|
+
waitFor: (target, waitOptions) => publicClaims.waitFor({ model: target.model, id: target.id }, waitOptions),
|
|
1366
1375
|
selfParticipantId: participantId,
|
|
1376
|
+
selfParticipantKind: kind,
|
|
1377
|
+
// Read-interest / write-intent enrolment for the typed surface.
|
|
1378
|
+
// `enterScope`/`pinScope` resolve the `{ [schemaKey]: id }` scope
|
|
1379
|
+
// through the SAME resolver the claim path uses, landing this client in
|
|
1380
|
+
// the entity-scoped group the holder's claim presence fans out on.
|
|
1381
|
+
// Return the store promise so the claim write path can AWAIT pinScope
|
|
1382
|
+
// BEFORE acquiring the lease (closing the subscribe-vs-broadcast race);
|
|
1383
|
+
// read-interest callers (`retrieve`/`claim.state`) still `void` it and
|
|
1384
|
+
// stay fire-and-forget. SOFT either way — the store swallows reconcile
|
|
1385
|
+
// errors so read interest never makes a read reject or stall.
|
|
1386
|
+
enterScope: (scope) => store.enterScope(scope),
|
|
1387
|
+
pinScope: (scope) => store.pinScope(scope),
|
|
1367
1388
|
});
|
|
1368
1389
|
}
|
|
1369
1390
|
const commits = {
|
|
@@ -1375,7 +1396,7 @@ export function Ablo(options) {
|
|
|
1375
1396
|
readAt: commitOptions.readAt,
|
|
1376
1397
|
onStale: commitOptions.onStale,
|
|
1377
1398
|
wait: commitOptions.wait,
|
|
1378
|
-
|
|
1399
|
+
claim: commitOptions.claim,
|
|
1379
1400
|
}, 'commits.create');
|
|
1380
1401
|
const clientTxId = createClientTxId(commitOptions.idempotencyKey);
|
|
1381
1402
|
// A claim handle supplies the batch stale-guard defaults — same
|
|
@@ -1388,8 +1409,8 @@ export function Ablo(options) {
|
|
|
1388
1409
|
onStale: commitOptions.onStale ?? (claim?.readAt !== undefined ? 'reject' : null),
|
|
1389
1410
|
});
|
|
1390
1411
|
const wait = commitOptions.wait ?? 'confirmed';
|
|
1391
|
-
const
|
|
1392
|
-
void
|
|
1412
|
+
const claimId = normalizeClaimId(commitOptions.claimRef) ?? claim?.claimId;
|
|
1413
|
+
void claimId; // The current wire clears claims by entity after commit.
|
|
1393
1414
|
// Route through the TransactionQueue's commit lane so the call
|
|
1394
1415
|
// tolerates WS disconnects: the envelope stays in memory until
|
|
1395
1416
|
// reconnect, mutationExecutor.commit() owns transport-level
|
|
@@ -1462,7 +1483,7 @@ export function Ablo(options) {
|
|
|
1462
1483
|
const id = params.id ?? createModelId();
|
|
1463
1484
|
await applyClaimedPolicy({ model: name, id }, params);
|
|
1464
1485
|
return commits.create({
|
|
1465
|
-
|
|
1486
|
+
claimRef: params.claimRef,
|
|
1466
1487
|
idempotencyKey: params.idempotencyKey,
|
|
1467
1488
|
readAt: params.readAt,
|
|
1468
1489
|
onStale: params.onStale,
|
|
@@ -1481,7 +1502,7 @@ export function Ablo(options) {
|
|
|
1481
1502
|
async update(params) {
|
|
1482
1503
|
await applyClaimedPolicy({ model: name, id: params.id }, params);
|
|
1483
1504
|
return commits.create({
|
|
1484
|
-
|
|
1505
|
+
claimRef: params.claimRef,
|
|
1485
1506
|
idempotencyKey: params.idempotencyKey,
|
|
1486
1507
|
readAt: params.readAt,
|
|
1487
1508
|
onStale: params.onStale,
|
|
@@ -1500,7 +1521,7 @@ export function Ablo(options) {
|
|
|
1500
1521
|
async delete(params) {
|
|
1501
1522
|
await applyClaimedPolicy({ model: name, id: params.id }, params);
|
|
1502
1523
|
return commits.create({
|
|
1503
|
-
|
|
1524
|
+
claimRef: params.claimRef,
|
|
1504
1525
|
idempotencyKey: params.idempotencyKey,
|
|
1505
1526
|
readAt: params.readAt,
|
|
1506
1527
|
onStale: params.onStale,
|
|
@@ -1659,7 +1680,7 @@ export function Ablo(options) {
|
|
|
1659
1680
|
logger.warn('Error during sync engine disposal', { error: err.message });
|
|
1660
1681
|
}
|
|
1661
1682
|
presenceStream.dispose();
|
|
1662
|
-
|
|
1683
|
+
claimStream.dispose();
|
|
1663
1684
|
syncClient.dispose();
|
|
1664
1685
|
},
|
|
1665
1686
|
/**
|
|
@@ -1711,8 +1732,8 @@ export function Ablo(options) {
|
|
|
1711
1732
|
/** Presence livestream — same socket as entity sync, no second
|
|
1712
1733
|
* connection. Stable reference across the engine's lifetime. */
|
|
1713
1734
|
presence: presenceStream,
|
|
1714
|
-
/**
|
|
1715
|
-
|
|
1735
|
+
/** Claim livestream — same socket. Stable reference. */
|
|
1736
|
+
claims: publicClaims,
|
|
1716
1737
|
commits,
|
|
1717
1738
|
model,
|
|
1718
1739
|
/** Structured multiplayer participation — target-first, no
|
|
@@ -5,16 +5,17 @@
|
|
|
5
5
|
* IndexedDB, no WebSocket. It maps the public Model / Claim / Commit
|
|
6
6
|
* nouns directly to HTTP routes on sync-server.
|
|
7
7
|
*/
|
|
8
|
-
import type { AbloOptions, CommitResource,
|
|
8
|
+
import type { AbloOptions, CommitResource, ClaimCreateOptions, ClaimWaitOptions, ModelClient, ModelClaim, ModelTarget } from './Ablo.js';
|
|
9
|
+
import type { ClaimHandle } from './createModelProxy.js';
|
|
9
10
|
import type { Duration } from '../utils/duration.js';
|
|
10
11
|
export type AbloApiClientOptions = Omit<AbloOptions, 'schema'> & {
|
|
11
12
|
readonly schema?: null | undefined;
|
|
12
13
|
readonly bootstrapBaseUrl?: string | undefined;
|
|
13
14
|
};
|
|
14
|
-
export interface
|
|
15
|
-
create(options:
|
|
15
|
+
export interface AbloApiClaims {
|
|
16
|
+
create(options: ClaimCreateOptions): Promise<ClaimHandle>;
|
|
16
17
|
list(target?: Partial<ModelTarget>): Promise<readonly ModelClaim[]>;
|
|
17
|
-
waitFor(target: Partial<ModelTarget>, options?:
|
|
18
|
+
waitFor(target: Partial<ModelTarget>, options?: ClaimWaitOptions): Promise<void>;
|
|
18
19
|
}
|
|
19
20
|
export type CapabilityParticipantKind = 'agent' | 'system';
|
|
20
21
|
export interface CapabilityCreateBaseOptions {
|
|
@@ -121,7 +122,7 @@ export interface AbloApi {
|
|
|
121
122
|
dispose(): Promise<void>;
|
|
122
123
|
purge(): Promise<void>;
|
|
123
124
|
readonly capabilities: CapabilityResource;
|
|
124
|
-
readonly
|
|
125
|
+
readonly claims: AbloApiClaims;
|
|
125
126
|
readonly commits: CommitResource;
|
|
126
127
|
model<T = Record<string, unknown>>(name: string): ModelClient<T>;
|
|
127
128
|
/**
|