@abloatai/ablo 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +72 -1
- package/README.md +80 -66
- package/dist/BaseSyncedStore.d.ts +73 -0
- package/dist/BaseSyncedStore.js +179 -5
- package/dist/Model.d.ts +42 -0
- package/dist/Model.js +103 -44
- package/dist/SyncEngineContext.d.ts +2 -1
- package/dist/SyncEngineContext.js +5 -3
- package/dist/agent/session.js +6 -5
- package/dist/ai-sdk/coordination-context.js +4 -0
- package/dist/ai-sdk/index.d.ts +56 -47
- package/dist/ai-sdk/index.js +56 -47
- package/dist/ai-sdk/intent-broadcast.d.ts +5 -0
- package/dist/ai-sdk/intent-broadcast.js +11 -4
- package/dist/ai-sdk/wrap.d.ts +14 -11
- package/dist/ai-sdk/wrap.js +11 -13
- package/dist/auth/credentialSource.d.ts +34 -0
- package/dist/auth/credentialSource.js +63 -0
- package/dist/auth/index.d.ts +2 -22
- package/dist/auth/index.js +26 -36
- package/dist/auth/schemas.d.ts +35 -0
- package/dist/auth/schemas.js +53 -0
- package/dist/client/Ablo.d.ts +259 -33
- package/dist/client/Ablo.js +276 -73
- package/dist/client/ApiClient.d.ts +52 -4
- package/dist/client/ApiClient.js +236 -66
- package/dist/client/auth.d.ts +21 -2
- package/dist/client/auth.js +77 -5
- package/dist/client/createInternalComponents.d.ts +2 -0
- package/dist/client/createInternalComponents.js +8 -1
- package/dist/client/createModelProxy.d.ts +187 -79
- package/dist/client/createModelProxy.js +203 -68
- package/dist/client/httpClient.d.ts +71 -0
- package/dist/client/httpClient.js +69 -0
- package/dist/client/identity.d.ts +2 -6
- package/dist/client/identity.js +63 -11
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -0
- package/dist/client/registerDataSource.d.ts +19 -0
- package/dist/client/registerDataSource.js +59 -0
- package/dist/client/validateAbloOptions.d.ts +2 -1
- package/dist/client/validateAbloOptions.js +8 -7
- package/dist/core/DatabaseManager.js +30 -2
- package/dist/core/openIDBWithTimeout.d.ts +36 -0
- package/dist/core/openIDBWithTimeout.js +88 -1
- package/dist/errorCodes.d.ts +92 -1
- package/dist/errorCodes.js +139 -7
- package/dist/errors.d.ts +54 -3
- package/dist/errors.js +192 -44
- package/dist/index.d.ts +23 -10
- package/dist/index.js +21 -8
- package/dist/keys/index.d.ts +76 -0
- package/dist/keys/index.js +171 -0
- package/dist/mutators/UndoManager.d.ts +86 -50
- package/dist/mutators/UndoManager.js +129 -22
- package/dist/mutators/inverseOp.d.ts +129 -0
- package/dist/mutators/inverseOp.js +74 -0
- package/dist/mutators/readerActions.d.ts +1 -1
- package/dist/mutators/undoApply.d.ts +42 -0
- package/dist/mutators/undoApply.js +143 -0
- package/dist/query/client.d.ts +10 -9
- package/dist/query/client.js +22 -14
- package/dist/react/AbloProvider.d.ts +23 -101
- package/dist/react/AbloProvider.js +61 -103
- package/dist/react/ClientSideSuspense.d.ts +1 -1
- package/dist/react/DefaultFallback.d.ts +1 -1
- package/dist/react/SyncGroupProvider.d.ts +1 -1
- package/dist/react/index.d.ts +3 -2
- package/dist/react/index.js +3 -2
- package/dist/react/useAblo.d.ts +4 -4
- package/dist/react/useAblo.js +10 -5
- package/dist/react/useCurrentUserId.d.ts +1 -1
- package/dist/react/useCurrentUserId.js +1 -1
- package/dist/react/useMutators.js +19 -12
- package/dist/react/useReactive.js +16 -3
- package/dist/schema/ddl.d.ts +26 -3
- package/dist/schema/ddl.js +152 -4
- package/dist/schema/index.d.ts +4 -0
- package/dist/schema/index.js +12 -0
- package/dist/schema/model.d.ts +11 -0
- package/dist/schema/model.js +2 -0
- package/dist/schema/openapi.d.ts +28 -0
- package/dist/schema/openapi.js +118 -0
- package/dist/schema/plane.d.ts +23 -0
- package/dist/schema/plane.js +19 -0
- package/dist/schema/relation.d.ts +20 -0
- package/dist/schema/serialize.d.ts +7 -3
- package/dist/schema/serialize.js +6 -2
- package/dist/schema/sync-delta-row.d.ts +157 -0
- package/dist/schema/sync-delta-row.js +102 -0
- package/dist/schema/sync-delta-wire.d.ts +180 -0
- package/dist/schema/sync-delta-wire.js +102 -0
- package/dist/server/adapter.d.ts +156 -0
- package/dist/server/adapter.js +19 -0
- package/dist/server/commit.d.ts +82 -0
- package/dist/server/commit.js +1 -0
- package/dist/server/index.d.ts +14 -0
- package/dist/server/index.js +1 -0
- package/dist/server/next.d.ts +51 -0
- package/dist/server/next.js +47 -0
- package/dist/server/read-config.d.ts +60 -0
- package/dist/server/read-config.js +8 -0
- package/dist/server/storage-mode.d.ts +17 -0
- package/dist/server/storage-mode.js +12 -0
- package/dist/source/adapter.d.ts +59 -0
- package/dist/source/adapter.js +19 -0
- package/dist/source/adapters/drizzle.d.ts +34 -0
- package/dist/source/adapters/drizzle.js +147 -0
- package/dist/source/adapters/memory.d.ts +12 -0
- package/dist/source/adapters/memory.js +114 -0
- package/dist/source/adapters/prisma.d.ts +57 -0
- package/dist/source/adapters/prisma.js +199 -0
- package/dist/source/conformance.d.ts +32 -0
- package/dist/source/conformance.js +134 -0
- package/dist/source/contract.d.ts +143 -0
- package/dist/source/contract.js +98 -0
- package/dist/source/index.d.ts +61 -10
- package/dist/source/index.js +98 -0
- package/dist/source/next.d.ts +33 -0
- package/dist/source/next.js +26 -0
- package/dist/sync/BootstrapHelper.d.ts +10 -0
- package/dist/sync/BootstrapHelper.js +56 -42
- package/dist/sync/ConnectionManager.d.ts +57 -1
- package/dist/sync/ConnectionManager.js +186 -11
- package/dist/sync/HydrationCoordinator.d.ts +93 -17
- package/dist/sync/HydrationCoordinator.js +241 -41
- package/dist/sync/NetworkProbe.d.ts +60 -18
- package/dist/sync/NetworkProbe.js +121 -23
- package/dist/sync/SyncWebSocket.d.ts +45 -70
- package/dist/sync/SyncWebSocket.js +113 -89
- package/dist/sync/createIntentStream.js +10 -1
- package/dist/sync/participants.js +5 -2
- package/dist/transactions/TransactionQueue.js +13 -1
- package/dist/types/streams.d.ts +9 -0
- package/dist/utils/mobx-setup.js +1 -0
- package/dist/webhooks/events.d.ts +38 -0
- package/dist/webhooks/events.js +40 -0
- package/dist/webhooks/index.d.ts +10 -0
- package/dist/webhooks/index.js +10 -0
- package/dist/wire/errorEnvelope.d.ts +34 -0
- package/dist/wire/errorEnvelope.js +86 -0
- package/dist/wire/frames.d.ts +119 -0
- package/dist/wire/frames.js +1 -0
- package/dist/wire/index.d.ts +24 -0
- package/dist/wire/index.js +21 -0
- package/dist/wire/listEnvelope.d.ts +45 -0
- package/dist/wire/listEnvelope.js +17 -0
- package/docs/api-keys.md +5 -5
- package/docs/api.md +125 -65
- package/docs/audit.md +16 -9
- package/docs/cli.md +57 -47
- package/docs/client-behavior.md +54 -40
- package/docs/coordination.md +66 -80
- package/docs/data-sources.md +56 -34
- package/docs/examples/agent-human.md +74 -28
- package/docs/examples/ai-sdk-tool.md +29 -22
- package/docs/examples/existing-python-backend.md +41 -26
- package/docs/examples/nextjs.md +32 -17
- package/docs/examples/scoped-agent.md +43 -28
- package/docs/examples/server-agent.md +40 -15
- package/docs/guarantees.md +38 -27
- package/docs/identity.md +65 -59
- package/docs/index.md +30 -19
- package/docs/integration-guide.md +78 -78
- package/docs/interaction-model.md +43 -35
- package/docs/mcp/claude-code.md +11 -19
- package/docs/mcp/cursor.md +7 -25
- package/docs/mcp/windsurf.md +7 -20
- package/docs/mcp.md +103 -26
- package/docs/quickstart.md +63 -61
- package/docs/react.md +24 -16
- package/docs/roadmap.md +13 -13
- package/docs/schema-contract.md +111 -0
- package/docs/the-loop.md +21 -0
- package/examples/README.md +8 -4
- package/examples/data-source/README.md +10 -7
- package/examples/data-source/customer-server.ts +27 -25
- package/examples/data-source/run.ts +4 -3
- package/examples/quickstart.ts +1 -1
- package/llms.txt +55 -21
- package/package.json +48 -3
|
@@ -7,13 +7,15 @@
|
|
|
7
7
|
* testable in isolation and the constructor doesn't carry it.
|
|
8
8
|
*
|
|
9
9
|
* Each schema model gets one `ModelOperations<T, CreateInput>` —
|
|
10
|
-
* exposes `retrieve
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* `
|
|
10
|
+
* exposes the async server reads `retrieve` / `list`, the synchronous
|
|
11
|
+
* local-graph snapshots `get` / `getAll` / `getCount`, the writes
|
|
12
|
+
* `create` / `update` / `delete`, the coordination namespace `claim`
|
|
13
|
+
* (`claim({ id })` plus `claim.state` / `claim.queue` / `claim.release` /
|
|
14
|
+
* `claim.reorder`), and `onChange`. The factory returns a plain object; the
|
|
15
|
+
* client assembles the `ablo.<model>` lookup table from these.
|
|
14
16
|
*/
|
|
15
17
|
import { autorun } from 'mobx';
|
|
16
|
-
import { AbloClaimedError, AbloValidationError } from '../errors.js';
|
|
18
|
+
import { AbloClaimedError, AbloValidationError, toAbloError, } from '../errors.js';
|
|
17
19
|
import { Model, modelAsRow } from '../Model.js';
|
|
18
20
|
import { ModelScope } from '../types/index.js';
|
|
19
21
|
const modelClientMeta = new WeakMap();
|
|
@@ -28,6 +30,22 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
28
30
|
throw new AbloValidationError(`Ablo: schema model "${schemaKey}" resolved to "${registeredModelName}", ` +
|
|
29
31
|
'but no matching constructor was registered.', { code: 'model_not_registered' });
|
|
30
32
|
}
|
|
33
|
+
// Last-line guarantee for the public surface: any rejection from a lower
|
|
34
|
+
// layer (transport timeout, IndexedDB failure, a third-party throw) is
|
|
35
|
+
// coerced to an AbloError before it reaches the consumer. The SDK's
|
|
36
|
+
// contract is that callers only ever catch tagged errors — `instanceof
|
|
37
|
+
// AbloError` / `e.type` always hold. Internal helpers stay unwrapped; only
|
|
38
|
+
// the methods exposed on `operations` are guarded.
|
|
39
|
+
const guard = (fn) => {
|
|
40
|
+
return async (...args) => {
|
|
41
|
+
try {
|
|
42
|
+
return await fn(...args);
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
throw toAbloError(err);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
};
|
|
31
49
|
const load = async (options) => {
|
|
32
50
|
const rows = await hydration.fetch(schemaKey, options);
|
|
33
51
|
// The coordinator returns Model instances. ModelOperations is
|
|
@@ -43,9 +61,23 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
43
61
|
await syncClient.waitForConfirmation(model.getModelName(), model.id);
|
|
44
62
|
};
|
|
45
63
|
// Claims this proxy currently holds, keyed by entity id. Lets the flat
|
|
46
|
-
// `release(id)` and `update(id)` find the lease + snapshot a `claim(id)`
|
|
64
|
+
// `release({ id })` and `update({ id, data })` find the lease + snapshot a `claim({ id })`
|
|
47
65
|
// took — no per-call handle. Released on dispose, explicit release, or TTL.
|
|
48
66
|
const activeClaims = new Map();
|
|
67
|
+
const isClaimHandle = (value) => typeof value === 'object' &&
|
|
68
|
+
value !== null &&
|
|
69
|
+
value.object === 'claim' &&
|
|
70
|
+
typeof value.claimId === 'string' &&
|
|
71
|
+
typeof value.release === 'function';
|
|
72
|
+
const claimMeta = (options) => {
|
|
73
|
+
if (!options?.description)
|
|
74
|
+
return options?.meta;
|
|
75
|
+
return { ...(options.meta ?? {}), description: options.description };
|
|
76
|
+
};
|
|
77
|
+
const mutationOptions = (params) => {
|
|
78
|
+
const { id: _id, data: _data, claim: _claim, ...rest } = params;
|
|
79
|
+
return rest;
|
|
80
|
+
};
|
|
49
81
|
const releaseClaim = async (id) => {
|
|
50
82
|
const held = activeClaims.get(id);
|
|
51
83
|
if (!held)
|
|
@@ -53,10 +85,11 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
53
85
|
activeClaims.delete(id);
|
|
54
86
|
await held.lease.release();
|
|
55
87
|
};
|
|
56
|
-
const takeClaim = async (
|
|
88
|
+
const takeClaim = async (params) => {
|
|
57
89
|
if (!collaboration) {
|
|
58
90
|
throw new AbloValidationError(`Model "${schemaKey}" cannot claim a row without collaboration wiring.`, { code: 'model_claim_not_configured' });
|
|
59
91
|
}
|
|
92
|
+
const { id, ...options } = params;
|
|
60
93
|
// Is someone ELSE already on this target? Read the local coordination
|
|
61
94
|
// snapshot up front — it decides whether we'll need to re-read after the
|
|
62
95
|
// claim (a free / already-mine target can't have changed under us).
|
|
@@ -91,6 +124,9 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
91
124
|
model: schemaKey,
|
|
92
125
|
id,
|
|
93
126
|
...(options?.field ? { field: options.field } : {}),
|
|
127
|
+
...(options?.path ? { path: options.path } : {}),
|
|
128
|
+
...(options?.range ? { range: options.range } : {}),
|
|
129
|
+
...(claimMeta(options) ? { meta: claimMeta(options) } : {}),
|
|
94
130
|
},
|
|
95
131
|
action: options?.action ?? 'editing',
|
|
96
132
|
ttl: options?.ttl,
|
|
@@ -106,34 +142,63 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
106
142
|
}
|
|
107
143
|
const snapshot = collaboration.createSnapshot(schemaKey, id);
|
|
108
144
|
activeClaims.set(id, { lease, snapshot });
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
145
|
+
const target = {
|
|
146
|
+
model: schemaKey,
|
|
147
|
+
id,
|
|
148
|
+
...(options?.field ? { field: options.field } : {}),
|
|
149
|
+
...(options?.path ? { path: options.path } : {}),
|
|
150
|
+
...(options?.range ? { range: options.range } : {}),
|
|
151
|
+
...(claimMeta(options) ? { meta: claimMeta(options) } : {}),
|
|
152
|
+
};
|
|
153
|
+
const release = () => releaseClaim(id);
|
|
154
|
+
return {
|
|
155
|
+
object: 'claim',
|
|
156
|
+
claimId: lease.id,
|
|
157
|
+
target,
|
|
158
|
+
action: options?.action ?? 'editing',
|
|
159
|
+
...(options?.description ? { description: options.description } : {}),
|
|
160
|
+
data: modelAsRow(model),
|
|
161
|
+
release,
|
|
162
|
+
revoke: () => {
|
|
163
|
+
void release();
|
|
164
|
+
},
|
|
165
|
+
[Symbol.asyncDispose]: release,
|
|
166
|
+
};
|
|
117
167
|
};
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
168
|
+
const claim = (params) => takeClaim(params);
|
|
169
|
+
// `claim` is a callable namespace: invoke it to take a claim, reach its
|
|
170
|
+
// members to read/steer the coordination plane. Attach the readers to the
|
|
171
|
+
// guarded callable so `ablo.<model>.claim(...)` and `ablo.<model>.claim.state(...)`
|
|
172
|
+
// are the same object.
|
|
173
|
+
const claimApi = Object.assign(guard(claim), {
|
|
174
|
+
state(params) {
|
|
175
|
+
return collaboration?.observe({ model: schemaKey, id: params.id }) ?? null;
|
|
176
|
+
},
|
|
177
|
+
queue(params) {
|
|
178
|
+
return {
|
|
179
|
+
object: 'list',
|
|
180
|
+
data: collaboration?.queue({ model: schemaKey, id: params.id }) ?? [],
|
|
181
|
+
};
|
|
182
|
+
},
|
|
183
|
+
reorder(params) {
|
|
184
|
+
collaboration?.reorder({ model: schemaKey, id: params.id }, params.order);
|
|
185
|
+
},
|
|
186
|
+
release: guard((params) => releaseClaim(isClaimHandle(params) ? params.target.id : params.id)),
|
|
187
|
+
});
|
|
132
188
|
const operations = {
|
|
133
|
-
retrieve(
|
|
189
|
+
retrieve: guard(async (params) => {
|
|
190
|
+
const rows = await load({
|
|
191
|
+
...params,
|
|
192
|
+
where: { id: params.id },
|
|
193
|
+
limit: 1,
|
|
194
|
+
});
|
|
195
|
+
return rows[0];
|
|
196
|
+
}),
|
|
197
|
+
list: guard(load),
|
|
198
|
+
get(id) {
|
|
134
199
|
return objectPool.get(id);
|
|
135
200
|
},
|
|
136
|
-
|
|
201
|
+
getAll(options) {
|
|
137
202
|
const all = objectPool.getByType(ModelClass, (options?.state ?? ModelScope.live));
|
|
138
203
|
let result = all;
|
|
139
204
|
if (options?.where) {
|
|
@@ -166,73 +231,143 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
166
231
|
result = result.slice(0, options.limit);
|
|
167
232
|
return result;
|
|
168
233
|
},
|
|
169
|
-
|
|
170
|
-
return this.
|
|
234
|
+
getCount(options) {
|
|
235
|
+
return this.getAll(options).length;
|
|
171
236
|
},
|
|
172
|
-
async
|
|
173
|
-
// TODO(options-persistence): stash `
|
|
237
|
+
create: guard(async (params) => {
|
|
238
|
+
// TODO(options-persistence): stash `params` alongside the
|
|
174
239
|
// queued transaction so idempotencyKey survives offline flush.
|
|
240
|
+
const id = params.id ?? Model.generateId();
|
|
241
|
+
const opts = mutationOptions(params);
|
|
242
|
+
const claim = params.claim;
|
|
243
|
+
let autoLease;
|
|
244
|
+
if (claim && !isClaimHandle(claim)) {
|
|
245
|
+
if (!collaboration) {
|
|
246
|
+
throw new AbloValidationError(`Model "${schemaKey}" cannot claim a row without collaboration wiring.`, { code: 'model_claim_not_configured' });
|
|
247
|
+
}
|
|
248
|
+
autoLease = await collaboration.createIntent({
|
|
249
|
+
target: {
|
|
250
|
+
model: schemaKey,
|
|
251
|
+
id,
|
|
252
|
+
...(claim.field ? { field: claim.field } : {}),
|
|
253
|
+
...(claim.path ? { path: claim.path } : {}),
|
|
254
|
+
...(claim.range ? { range: claim.range } : {}),
|
|
255
|
+
...(claimMeta(claim) ? { meta: claimMeta(claim) } : {}),
|
|
256
|
+
},
|
|
257
|
+
action: claim.action ?? 'creating',
|
|
258
|
+
ttl: claim.ttl,
|
|
259
|
+
queue: claim.wait !== false,
|
|
260
|
+
maxQueueDepth: claim.maxQueueDepth,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
175
263
|
const model = new ModelClass({
|
|
176
|
-
id
|
|
177
|
-
...data,
|
|
264
|
+
id,
|
|
265
|
+
...params.data,
|
|
178
266
|
createdAt: new Date(),
|
|
179
267
|
updatedAt: new Date(),
|
|
180
268
|
});
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
269
|
+
const effective = {
|
|
270
|
+
...opts,
|
|
271
|
+
...(autoLease ? { intent: autoLease } : {}),
|
|
272
|
+
...(isClaimHandle(claim) ? { intent: { id: claim.claimId } } : {}),
|
|
273
|
+
};
|
|
274
|
+
try {
|
|
275
|
+
syncClient.add(model, effective);
|
|
276
|
+
await waitForMutation(model, effective);
|
|
277
|
+
return modelAsRow(model);
|
|
278
|
+
}
|
|
279
|
+
finally {
|
|
280
|
+
await autoLease?.release().catch(() => { });
|
|
281
|
+
}
|
|
282
|
+
}),
|
|
283
|
+
update: guard(async (params) => {
|
|
284
|
+
const autoClaim = params.claim && !isClaimHandle(params.claim) ? params.claim : null;
|
|
285
|
+
if (autoClaim) {
|
|
286
|
+
const handle = await takeClaim({ ...autoClaim, id: params.id });
|
|
287
|
+
try {
|
|
288
|
+
return await operations.update({ ...params, claim: handle });
|
|
289
|
+
}
|
|
290
|
+
finally {
|
|
291
|
+
await handle.release();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const { id } = params;
|
|
186
295
|
const model = objectPool.get(id);
|
|
187
296
|
if (!model)
|
|
188
297
|
throw new AbloValidationError(`Entity not found: ${registeredModelName}/${id}`, { code: 'entity_not_found' });
|
|
189
298
|
// If we hold a claim on this row, guard the write with its snapshot
|
|
190
299
|
// watermark + lease so it's stale-rejected and attributed to the claim.
|
|
191
300
|
const claimed = activeClaims.get(id);
|
|
301
|
+
const opts = mutationOptions(params);
|
|
302
|
+
const handleIntent = isClaimHandle(params.claim)
|
|
303
|
+
? { id: params.claim.claimId }
|
|
304
|
+
: undefined;
|
|
192
305
|
const effective = claimed
|
|
193
306
|
? {
|
|
194
307
|
wait: 'confirmed',
|
|
195
308
|
readAt: claimed.snapshot.stamp,
|
|
196
309
|
onStale: 'reject',
|
|
197
310
|
intent: claimed.lease,
|
|
198
|
-
...
|
|
311
|
+
...opts,
|
|
199
312
|
}
|
|
200
|
-
:
|
|
201
|
-
|
|
313
|
+
: {
|
|
314
|
+
...opts,
|
|
315
|
+
...(handleIntent ? { intent: handleIntent } : {}),
|
|
316
|
+
};
|
|
317
|
+
// Local user update: `applyChanges` keeps change tracking ON so
|
|
318
|
+
// the edited fields land in `modifiedProperties` and actually get
|
|
319
|
+
// sent to the server. (`updateFromData` is the hydration path and
|
|
320
|
+
// would discard the tracking → empty `input: {}` no-op mutation.)
|
|
321
|
+
model.applyChanges(params.data);
|
|
202
322
|
syncClient.update(model, effective);
|
|
203
323
|
await waitForMutation(model, effective);
|
|
204
324
|
return modelAsRow(model);
|
|
205
|
-
},
|
|
206
|
-
async
|
|
325
|
+
}),
|
|
326
|
+
delete: guard(async (params) => {
|
|
327
|
+
const autoClaim = params.claim && !isClaimHandle(params.claim) ? params.claim : null;
|
|
328
|
+
if (autoClaim) {
|
|
329
|
+
const handle = await takeClaim({ ...autoClaim, id: params.id });
|
|
330
|
+
try {
|
|
331
|
+
await operations.delete({ ...params, claim: handle });
|
|
332
|
+
}
|
|
333
|
+
finally {
|
|
334
|
+
await handle.release();
|
|
335
|
+
}
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const { id } = params;
|
|
207
339
|
const model = objectPool.get(id);
|
|
208
340
|
if (!model)
|
|
209
341
|
throw new AbloValidationError(`Entity not found: ${registeredModelName}/${id}`, { code: 'entity_not_found' });
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
342
|
+
const claimed = activeClaims.get(id);
|
|
343
|
+
const opts = mutationOptions(params);
|
|
344
|
+
const handleIntent = isClaimHandle(params.claim)
|
|
345
|
+
? { id: params.claim.claimId }
|
|
346
|
+
: undefined;
|
|
347
|
+
const effective = claimed
|
|
348
|
+
? {
|
|
349
|
+
wait: 'confirmed',
|
|
350
|
+
readAt: claimed.snapshot.stamp,
|
|
351
|
+
onStale: 'reject',
|
|
352
|
+
intent: claimed.lease,
|
|
353
|
+
...opts,
|
|
354
|
+
}
|
|
355
|
+
: {
|
|
356
|
+
...opts,
|
|
357
|
+
...(handleIntent ? { intent: handleIntent } : {}),
|
|
358
|
+
};
|
|
359
|
+
syncClient.delete(model, effective);
|
|
360
|
+
await waitForMutation(model, effective);
|
|
361
|
+
}),
|
|
362
|
+
// `claim` is a callable namespace (take a claim) carrying the coordination
|
|
363
|
+
// readers (`claim.state` / `claim.queue` / `claim.release` / `claim.reorder`).
|
|
364
|
+
claim: claimApi,
|
|
229
365
|
onChange(callback, options) {
|
|
230
366
|
return autorun(() => {
|
|
231
|
-
const entities = this.
|
|
367
|
+
const entities = this.getAll(options);
|
|
232
368
|
callback(entities);
|
|
233
369
|
});
|
|
234
370
|
},
|
|
235
|
-
load,
|
|
236
371
|
};
|
|
237
372
|
modelClientMeta.set(operations, {
|
|
238
373
|
key: schemaKey,
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createAbloHttpClient` — a STATELESS, typed HTTP client for server-side actors
|
|
3
|
+
* (agents, workers, serverless), modelled on `@liveblocks/node` / the Stripe
|
|
4
|
+
* server SDK / Netflix Conductor workers: it talks to Ablo over plain HTTP with
|
|
5
|
+
* the credential as identity, and holds **no WebSocket and no connection state**.
|
|
6
|
+
*
|
|
7
|
+
* Why this exists (docs/plans/agent-transport-event-driven.md): the stateful
|
|
8
|
+
* `Ablo({ schema })` client is for INTERACTIVE participants — it opens a
|
|
9
|
+
* WebSocket and seeds its identity (userId/orgId) during the connect/bootstrap
|
|
10
|
+
* step, then routes writes through a `TransactionQueue` that drops mutations
|
|
11
|
+
* until that identity exists. A reactive agent has no socket, so that identity is
|
|
12
|
+
* never seeded and writes drop. The proven fix (unanimous across Liveblocks,
|
|
13
|
+
* Stripe, PlanetScale, Conductor, Better Auth) is NOT to de-socket the stateful
|
|
14
|
+
* client — it's a separate stateless client where the credential carries identity
|
|
15
|
+
* and the SERVER resolves it per request.
|
|
16
|
+
*
|
|
17
|
+
* Ablo already has that stateless surface: `Ablo({ schema: null })` returns the
|
|
18
|
+
* protocol client (`createProtocolClient` → `AbloApi`), which commits via
|
|
19
|
+
* `POST /v1/commits` and reads via the HTTP `ApiClient`, authenticating with the
|
|
20
|
+
* Bearer token on every request. Its only ergonomic gap is that model access is
|
|
21
|
+
* string-keyed (`api.model('slides')`) rather than typed (`api.slides`). This
|
|
22
|
+
* wraps it in a typed proxy facade so server code gets the SAME `client.<model>`
|
|
23
|
+
* surface as the browser client — typed proxies, stateless transport.
|
|
24
|
+
*/
|
|
25
|
+
import { type AbloApiClientOptions, type TaskCreateOptions } from './ApiClient.js';
|
|
26
|
+
import type { CommitReceipt, CommitResource, HttpClaimApi, ModelRead, ModelReadOptions, Turn } from './Ablo.js';
|
|
27
|
+
import type { ModelCreateParams, ModelDeleteParams, ModelLoadOptions, ModelRetrieveParams, ModelUpdateParams } from './createModelProxy.js';
|
|
28
|
+
import type { Schema, SchemaRecord, InferModel, InferCreate } from '../schema/schema.js';
|
|
29
|
+
export interface AbloHttpClientOptions<S extends SchemaRecord> extends Omit<AbloApiClientOptions, 'schema'> {
|
|
30
|
+
/** The schema — used for TYPING only (typed model proxies); never sent or used at runtime. */
|
|
31
|
+
readonly schema: Schema<S>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The per-model HTTP surface — exactly what a stateless client can do over
|
|
35
|
+
* request/response: reads (`retrieve`/`list`), writes (`create`/`update`/`delete`),
|
|
36
|
+
* and the durable-lease claim plane (`claim` — acquire/hold/release). It does NOT
|
|
37
|
+
* include `get`/`getAll`/`getCount` (local synced-pool reads) or `onChange` (live
|
|
38
|
+
* subscription); those need the stateful plane and are absent BY TYPE here.
|
|
39
|
+
*/
|
|
40
|
+
export interface HttpModelClient<T, C = T> {
|
|
41
|
+
retrieve(params: ModelRetrieveParams & ModelReadOptions): Promise<ModelRead<T>>;
|
|
42
|
+
list(options?: ModelLoadOptions<T>): Promise<T[]>;
|
|
43
|
+
create(params: ModelCreateParams<T, C>): Promise<CommitReceipt>;
|
|
44
|
+
update(params: ModelUpdateParams<C>): Promise<CommitReceipt>;
|
|
45
|
+
delete(params: ModelDeleteParams<T>): Promise<CommitReceipt>;
|
|
46
|
+
claim: HttpClaimApi<T>;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The honest type of the stateless HTTP client: typed model proxies (the
|
|
50
|
+
* request/response subset) + `commits` + `beginTurn` + `dispose`. Reaching for a
|
|
51
|
+
* stateful-only capability (`get`/`getAll`/`getCount`, `onChange`,
|
|
52
|
+
* `claim.state`/`queue`/`reorder`) is a COMPILE error here, not a latent runtime
|
|
53
|
+
* `undefined` — the type matches what the transport can actually do.
|
|
54
|
+
*/
|
|
55
|
+
export type AbloHttpClient<S extends SchemaRecord> = {
|
|
56
|
+
readonly [K in keyof S & string]: HttpModelClient<InferModel<Schema<S>, K>, InferCreate<Schema<S>, K>>;
|
|
57
|
+
} & {
|
|
58
|
+
readonly commits: CommitResource;
|
|
59
|
+
beginTurn(options: TaskCreateOptions): Promise<Turn>;
|
|
60
|
+
dispose(): Promise<void>;
|
|
61
|
+
/** Resolve the bearer credential this client authenticates with (see `AbloApi.getAuthToken`). */
|
|
62
|
+
getAuthToken(): Promise<string | null>;
|
|
63
|
+
/** String-keyed model accessor (for dynamic model names). */
|
|
64
|
+
model<T = Record<string, unknown>>(name: string): HttpModelClient<T>;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Stateless, typed HTTP client. Each `client.<model>` resolves to the protocol
|
|
68
|
+
* client's `model(name)`; `commits`, `beginTurn`, `tasks`, `dispose`, etc. pass
|
|
69
|
+
* through. No socket is ever opened; identity is the Bearer credential.
|
|
70
|
+
*/
|
|
71
|
+
export declare function createAbloHttpClient<S extends SchemaRecord>(options: AbloHttpClientOptions<S>): AbloHttpClient<S>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createAbloHttpClient` — a STATELESS, typed HTTP client for server-side actors
|
|
3
|
+
* (agents, workers, serverless), modelled on `@liveblocks/node` / the Stripe
|
|
4
|
+
* server SDK / Netflix Conductor workers: it talks to Ablo over plain HTTP with
|
|
5
|
+
* the credential as identity, and holds **no WebSocket and no connection state**.
|
|
6
|
+
*
|
|
7
|
+
* Why this exists (docs/plans/agent-transport-event-driven.md): the stateful
|
|
8
|
+
* `Ablo({ schema })` client is for INTERACTIVE participants — it opens a
|
|
9
|
+
* WebSocket and seeds its identity (userId/orgId) during the connect/bootstrap
|
|
10
|
+
* step, then routes writes through a `TransactionQueue` that drops mutations
|
|
11
|
+
* until that identity exists. A reactive agent has no socket, so that identity is
|
|
12
|
+
* never seeded and writes drop. The proven fix (unanimous across Liveblocks,
|
|
13
|
+
* Stripe, PlanetScale, Conductor, Better Auth) is NOT to de-socket the stateful
|
|
14
|
+
* client — it's a separate stateless client where the credential carries identity
|
|
15
|
+
* and the SERVER resolves it per request.
|
|
16
|
+
*
|
|
17
|
+
* Ablo already has that stateless surface: `Ablo({ schema: null })` returns the
|
|
18
|
+
* protocol client (`createProtocolClient` → `AbloApi`), which commits via
|
|
19
|
+
* `POST /v1/commits` and reads via the HTTP `ApiClient`, authenticating with the
|
|
20
|
+
* Bearer token on every request. Its only ergonomic gap is that model access is
|
|
21
|
+
* string-keyed (`api.model('slides')`) rather than typed (`api.slides`). This
|
|
22
|
+
* wraps it in a typed proxy facade so server code gets the SAME `client.<model>`
|
|
23
|
+
* surface as the browser client — typed proxies, stateless transport.
|
|
24
|
+
*/
|
|
25
|
+
import { createProtocolClient, } from './ApiClient.js';
|
|
26
|
+
/**
|
|
27
|
+
* Members of the underlying `AbloApi` that pass straight through the facade.
|
|
28
|
+
* Deliberately EXCLUDES the resource names that collide with common schema model
|
|
29
|
+
* names — `tasks`, `intents`, `capabilities`, `agent` — so `client.tasks` resolves
|
|
30
|
+
* to the schema model `tasks`, not the protocol `TaskResource`. Only lifecycle +
|
|
31
|
+
* the genuinely-protocol methods an agent uses pass through.
|
|
32
|
+
*/
|
|
33
|
+
const PROTOCOL_MEMBERS = new Set([
|
|
34
|
+
'ready',
|
|
35
|
+
'waitForFlush',
|
|
36
|
+
'dispose',
|
|
37
|
+
'purge',
|
|
38
|
+
'commits',
|
|
39
|
+
'beginTurn',
|
|
40
|
+
'model',
|
|
41
|
+
'getAuthToken',
|
|
42
|
+
]);
|
|
43
|
+
/**
|
|
44
|
+
* Stateless, typed HTTP client. Each `client.<model>` resolves to the protocol
|
|
45
|
+
* client's `model(name)`; `commits`, `beginTurn`, `tasks`, `dispose`, etc. pass
|
|
46
|
+
* through. No socket is ever opened; identity is the Bearer credential.
|
|
47
|
+
*/
|
|
48
|
+
export function createAbloHttpClient(options) {
|
|
49
|
+
// The schema is type-level only; the protocol client is schema-agnostic.
|
|
50
|
+
const { schema: _schema, ...rest } = options;
|
|
51
|
+
const api = createProtocolClient({ ...rest, schema: null });
|
|
52
|
+
const facade = new Proxy(api, {
|
|
53
|
+
get(target, prop) {
|
|
54
|
+
if (typeof prop !== 'string')
|
|
55
|
+
return Reflect.get(target, prop);
|
|
56
|
+
// Real protocol members pass through unchanged.
|
|
57
|
+
if (PROTOCOL_MEMBERS.has(prop) && prop in target)
|
|
58
|
+
return Reflect.get(target, prop);
|
|
59
|
+
// Anything else is a typed model accessor → the string-keyed protocol model
|
|
60
|
+
// (which implements retrieve/list/create/update/delete/claim — every method
|
|
61
|
+
// `HttpModelClient` declares).
|
|
62
|
+
return api.model(prop);
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
// One boundary cast — and now an HONEST one: `AbloHttpClient<S>` declares only
|
|
66
|
+
// what `api.model()` + the passed-through protocol members actually implement,
|
|
67
|
+
// so there is no method on this type that fails at runtime.
|
|
68
|
+
return facade;
|
|
69
|
+
}
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import { type RefreshScheduler } from '../auth/index.js';
|
|
23
23
|
import type { BootstrapHelper } from '../sync/BootstrapHelper.js';
|
|
24
24
|
import type { SyncLogger } from '../interfaces/index.js';
|
|
25
|
+
import type { AuthCredentialSource } from '../auth/credentialSource.js';
|
|
25
26
|
import type { ApiKeySetter } from './auth.js';
|
|
26
27
|
export interface IdentityResolveInput {
|
|
27
28
|
readonly options: {
|
|
@@ -42,13 +43,8 @@ export interface IdentityResolveInput {
|
|
|
42
43
|
readonly configuredApiKey: string | ApiKeySetter | null;
|
|
43
44
|
readonly configuredAuthToken: string | null;
|
|
44
45
|
readonly bootstrapHelper: BootstrapHelper;
|
|
46
|
+
readonly auth: AuthCredentialSource;
|
|
45
47
|
readonly logger: SyncLogger;
|
|
46
|
-
/**
|
|
47
|
-
* Called when the hosted-cloud refresh scheduler rotates the
|
|
48
|
-
* capability token — caller forwards the new token to the live
|
|
49
|
-
* WebSocket via `ws.setCapabilityToken(...)`.
|
|
50
|
-
*/
|
|
51
|
-
readonly applyRotatedToken: (token: string) => void;
|
|
52
48
|
}
|
|
53
49
|
export interface ResolvedIdentity {
|
|
54
50
|
readonly userId: string;
|
package/dist/client/identity.js
CHANGED
|
@@ -23,11 +23,43 @@ import { AbloAuthenticationError } from '../errors.js';
|
|
|
23
23
|
import { exchangeApiKey } from '../auth/index.js';
|
|
24
24
|
import { resolveIdentity } from '../auth/index.js';
|
|
25
25
|
import { createRefreshScheduler, } from '../auth/index.js';
|
|
26
|
-
import { resolveApiKeyValue } from './auth.js';
|
|
26
|
+
import { resolveApiKeyValue, resolveBootstrapBaseUrl } from './auth.js';
|
|
27
27
|
export async function resolveParticipantIdentity(input) {
|
|
28
|
-
const { options, internalOptions, url, kind, configuredApiKey, configuredAuthToken, bootstrapHelper,
|
|
28
|
+
const { options, internalOptions, url, kind, configuredApiKey, configuredAuthToken, bootstrapHelper, auth, logger, } = input;
|
|
29
29
|
const apiKeyValue = await resolveApiKeyValue(configuredApiKey);
|
|
30
30
|
const initialCapToken = options.capabilityToken ?? configuredAuthToken ?? undefined;
|
|
31
|
+
// Branch 0: publishable key (`pk_`) — a long-lived, browser-safe, READ-ONLY
|
|
32
|
+
// project key. Unlike a secret `sk_` (Branch 1), it is used DIRECTLY as the
|
|
33
|
+
// bearer and is NEVER exchanged for a short-lived capability — so it never
|
|
34
|
+
// expires and there is nothing to refresh (no `credential_stale`, no
|
|
35
|
+
// wake-from-sleep re-mint). The sync-server's `apiKeyProvider` resolves the
|
|
36
|
+
// org + read-only scope from the key itself; we still call `/auth/identity`
|
|
37
|
+
// (authenticated by the `pk_` bearer) to learn the account scope + syncGroups
|
|
38
|
+
// for the bootstrap cache. Plain `startsWith` check because the `keys` module
|
|
39
|
+
// is node-only (`node:crypto`) and must not enter the browser bundle.
|
|
40
|
+
if (apiKeyValue && apiKeyValue.startsWith('pk_') && !options.capabilityToken) {
|
|
41
|
+
const baseUrl = resolveBootstrapBaseUrl({
|
|
42
|
+
url,
|
|
43
|
+
bootstrapBaseUrl: options.bootstrapBaseUrl,
|
|
44
|
+
});
|
|
45
|
+
const identity = await resolveIdentity({ baseUrl, authToken: apiKeyValue });
|
|
46
|
+
const callerGroups = options.syncGroups ?? [];
|
|
47
|
+
const mergedSyncGroups = callerGroups.length > 0
|
|
48
|
+
? [...new Set([...callerGroups, ...identity.syncGroups])]
|
|
49
|
+
: identity.syncGroups;
|
|
50
|
+
bootstrapHelper.setCacheScope(identity.accountScope);
|
|
51
|
+
bootstrapHelper.setSyncGroups(mergedSyncGroups);
|
|
52
|
+
auth.setAuthToken(apiKeyValue);
|
|
53
|
+
return {
|
|
54
|
+
userId: identity.participantId,
|
|
55
|
+
accountScope: identity.accountScope,
|
|
56
|
+
teamIds: undefined,
|
|
57
|
+
capabilityToken: apiKeyValue,
|
|
58
|
+
syncGroups: mergedSyncGroups,
|
|
59
|
+
participantKind: identity.participantKind,
|
|
60
|
+
refreshScheduler: null,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
31
63
|
// Branch 1: hosted-cloud (apiKey only, no caller-supplied capability token)
|
|
32
64
|
if (apiKeyValue && !options.capabilityToken) {
|
|
33
65
|
return resolveHosted({
|
|
@@ -37,14 +69,33 @@ export async function resolveParticipantIdentity(input) {
|
|
|
37
69
|
kind,
|
|
38
70
|
options,
|
|
39
71
|
bootstrapHelper,
|
|
72
|
+
auth,
|
|
40
73
|
logger,
|
|
41
|
-
applyRotatedToken,
|
|
42
74
|
});
|
|
43
75
|
}
|
|
44
76
|
// Branch 2: self-derived (capability token present, identity unknown)
|
|
45
77
|
if (!internalOptions.organizationId ||
|
|
46
78
|
(kind === 'agent' ? !options.agentId : !options.user?.id)) {
|
|
47
|
-
|
|
79
|
+
// Fail fast on the missing-credential case. We're here because there's no
|
|
80
|
+
// apiKey (Branch 1) and the identity isn't caller-supplied (Branch 3), so
|
|
81
|
+
// `initialCapToken` is the only thing that can authenticate the
|
|
82
|
+
// `/auth/identity` call. When it's absent — the common cause being
|
|
83
|
+
// `getToken()` resolving to `null` (no/expired session, see
|
|
84
|
+
// `getSyncCapabilityToken`) — the request can only come back as the server's
|
|
85
|
+
// opaque `identity_resolve_failed: no_matching_provider`. Surface the real
|
|
86
|
+
// condition locally instead: `session_expired` is the registered,
|
|
87
|
+
// re-authenticate-able code, and we never make a doomed round-trip.
|
|
88
|
+
if (!initialCapToken) {
|
|
89
|
+
throw new AbloAuthenticationError('No auth token available to resolve identity — the session token is ' +
|
|
90
|
+
'missing or expired. Ensure `getToken()` returns a valid token, or ' +
|
|
91
|
+
'pass `apiKey` / `capabilityToken`.', { code: 'session_expired' });
|
|
92
|
+
}
|
|
93
|
+
// Single source of truth for the http(s) base — coerces ws/wss → http/https
|
|
94
|
+
// even when `bootstrapBaseUrl` is an explicit override (see auth.ts).
|
|
95
|
+
const baseUrl = resolveBootstrapBaseUrl({
|
|
96
|
+
url,
|
|
97
|
+
bootstrapBaseUrl: options.bootstrapBaseUrl,
|
|
98
|
+
});
|
|
48
99
|
const identity = await resolveIdentity({
|
|
49
100
|
baseUrl,
|
|
50
101
|
authToken: initialCapToken,
|
|
@@ -66,7 +117,7 @@ export async function resolveParticipantIdentity(input) {
|
|
|
66
117
|
: identity.syncGroups;
|
|
67
118
|
bootstrapHelper.setCacheScope(identity.accountScope);
|
|
68
119
|
bootstrapHelper.setSyncGroups(mergedSyncGroups);
|
|
69
|
-
|
|
120
|
+
auth.setAuthToken(initialCapToken);
|
|
70
121
|
return {
|
|
71
122
|
userId: identity.participantId,
|
|
72
123
|
accountScope: identity.accountScope,
|
|
@@ -83,7 +134,7 @@ export async function resolveParticipantIdentity(input) {
|
|
|
83
134
|
const accountScope = internalOptions.organizationId;
|
|
84
135
|
bootstrapHelper.setCacheScope(accountScope);
|
|
85
136
|
bootstrapHelper.setSyncGroups(options.syncGroups);
|
|
86
|
-
|
|
137
|
+
auth.setAuthToken(initialCapToken);
|
|
87
138
|
return {
|
|
88
139
|
userId,
|
|
89
140
|
accountScope,
|
|
@@ -97,8 +148,10 @@ export async function resolveParticipantIdentity(input) {
|
|
|
97
148
|
async function resolveHosted(input) {
|
|
98
149
|
// Pure managed-cloud shape: `Ablo({schema, apiKey})`. Server returns
|
|
99
150
|
// scope + userMeta; SDK populates internals.
|
|
100
|
-
const baseUrl =
|
|
101
|
-
|
|
151
|
+
const baseUrl = resolveBootstrapBaseUrl({
|
|
152
|
+
url: input.url,
|
|
153
|
+
bootstrapBaseUrl: input.options.bootstrapBaseUrl,
|
|
154
|
+
});
|
|
102
155
|
const exchangeArgs = {
|
|
103
156
|
baseUrl,
|
|
104
157
|
participantKind: (input.kind === 'agent' ? 'agent' : 'system'),
|
|
@@ -112,7 +165,7 @@ async function resolveHosted(input) {
|
|
|
112
165
|
});
|
|
113
166
|
input.bootstrapHelper.setCacheScope(exchange.scope.organizationId);
|
|
114
167
|
input.bootstrapHelper.setSyncGroups(exchange.scope.syncGroups);
|
|
115
|
-
input.
|
|
168
|
+
input.auth.setAuthToken(exchange.token);
|
|
116
169
|
// Cap tokens have a server-set TTL (3600s by default). Without
|
|
117
170
|
// proactive refresh the WS would either get force-closed at expiry
|
|
118
171
|
// or fail its next reconnect with 401. The scheduler re-mints
|
|
@@ -132,8 +185,7 @@ async function resolveHosted(input) {
|
|
|
132
185
|
...exchangeArgs,
|
|
133
186
|
apiKey: freshApiKey,
|
|
134
187
|
});
|
|
135
|
-
input.
|
|
136
|
-
input.applyRotatedToken(next.token);
|
|
188
|
+
input.auth.setAuthToken(next.token);
|
|
137
189
|
return { expiresAtMs: Date.parse(next.expiresAt) };
|
|
138
190
|
},
|
|
139
191
|
onError: (err) => {
|