@phosphorco/bb-identity 0.1.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.
@@ -0,0 +1,950 @@
1
+ // @bun
2
+ // model-runtime.ts
3
+ var ID_MAX_LENGTH = 256;
4
+ var ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@/-]*$/;
5
+ function failure(code, message, retry = "never") {
6
+ return { code, message, retry };
7
+ }
8
+ function ok(value) {
9
+ return { ok: true, value };
10
+ }
11
+ function err(code, message, retry = "never") {
12
+ return { ok: false, error: failure(code, message, retry) };
13
+ }
14
+ function isBoundedString(value, maximum = ID_MAX_LENGTH) {
15
+ return typeof value === "string" && value.length > 0 && value.length <= maximum;
16
+ }
17
+ function idCodec(kind) {
18
+ return {
19
+ decode(input) {
20
+ const valid = kind === "server-session" ? isBoundedString(input, 4096) && !/[\u0000-\u0020\u007f]/.test(input) : isBoundedString(input) && ID_PATTERN.test(input);
21
+ if (!valid) {
22
+ return err("invalid-input", `Invalid ${kind} identifier.`);
23
+ }
24
+ return ok(input);
25
+ },
26
+ encode(value) {
27
+ return value;
28
+ }
29
+ };
30
+ }
31
+ var identityKeyCodec = {
32
+ decode(input) {
33
+ if (!isBoundedString(input, 512) || /[\u0000-\u001f\u007f]/.test(input)) {
34
+ return err("invalid-input", "Invalid identity key.");
35
+ }
36
+ return ok(input);
37
+ },
38
+ encode(value) {
39
+ return value;
40
+ }
41
+ };
42
+ function newOperationId() {
43
+ const random = globalThis.crypto?.randomUUID?.() ?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
44
+ return `op-${random}`;
45
+ }
46
+ function newConflictToken() {
47
+ return `conflict-${globalThis.crypto?.randomUUID?.() ?? Date.now().toString(36)}`;
48
+ }
49
+
50
+ // state-service-runtime.ts
51
+ function isRecord(value) {
52
+ return typeof value === "object" && value !== null && !Array.isArray(value);
53
+ }
54
+ function nonEmpty(value) {
55
+ return typeof value === "string" && value.length > 0;
56
+ }
57
+ function decodeAddress(value) {
58
+ if (!isRecord(value) || !nonEmpty(value.collection) || !nonEmpty(value.recordId))
59
+ return err("invalid-input", "Invalid state address.");
60
+ const instanceId = idCodec("instance").decode(value.instanceId);
61
+ const pluginId = idCodec("plugin").decode(value.pluginId);
62
+ const owner = identityKeyCodec.decode(value.owner);
63
+ return instanceId.ok && pluginId.ok && owner.ok ? ok({ instanceId: instanceId.value, pluginId: pluginId.value, collection: value.collection, recordId: value.recordId, owner: owner.value }) : err("invalid-input", "Invalid state address identifiers.");
64
+ }
65
+ function decodeVersion(value) {
66
+ return isRecord(value) && nonEmpty(value.epoch) && typeof value.sequence === "number" && Number.isInteger(value.sequence) && value.sequence >= 0 ? ok({ epoch: value.epoch, sequence: value.sequence }) : err("invalid-input", "Invalid state version.");
67
+ }
68
+ function decodeActor(value) {
69
+ if (value === null)
70
+ return ok(null);
71
+ if (!isRecord(value) || !isRecord(value.identity) || !isRecord(value.presentation) || !nonEmpty(value.presentation.displayName) || value.presentation.handle !== null && typeof value.presentation.handle !== "string" || value.presentation.avatarUrl !== null && typeof value.presentation.avatarUrl !== "string" || !["provider-verified", "local-user", "upstream-default", "integration-asserted", "legacy", "machine"].includes(value.evidence)) {
72
+ return err("invalid-input", "Invalid state editor snapshot.");
73
+ }
74
+ const key = identityKeyCodec.decode(value.identity.key);
75
+ if (!key.ok)
76
+ return key;
77
+ const identity = value.identity;
78
+ const presentation = { displayName: value.presentation.displayName, handle: value.presentation.handle, avatarUrl: value.presentation.avatarUrl };
79
+ if (identity.kind === "machine") {
80
+ const instanceId = idCodec("instance").decode(identity.instanceId);
81
+ if (!instanceId.ok || value.evidence !== "machine" || identity.hostId !== null && (typeof identity.hostId !== "string" || identity.hostId.length === 0 || identity.hostId.length > 512)) {
82
+ return err("invalid-input", "Invalid machine editor identity.");
83
+ }
84
+ return ok({ identity: { kind: "machine", key: key.value, instanceId: instanceId.value, hostId: identity.hostId }, presentation, evidence: "machine" });
85
+ }
86
+ if (value.evidence === "machine")
87
+ return err("invalid-input", "Machine evidence requires a machine identity.");
88
+ if (identity.kind === "person" && nonEmpty(identity.issuer) && nonEmpty(identity.subject)) {
89
+ return ok({ identity: { kind: "person", key: key.value, issuer: identity.issuer, subject: identity.subject }, presentation, evidence: value.evidence });
90
+ }
91
+ if (identity.kind === "default-user") {
92
+ const instanceId = idCodec("instance").decode(identity.instanceId);
93
+ return instanceId.ok ? ok({ identity: { kind: "default-user", key: key.value, instanceId: instanceId.value }, presentation, evidence: value.evidence }) : instanceId;
94
+ }
95
+ if (identity.kind === "external") {
96
+ const pluginId = idCodec("plugin").decode(identity.pluginId);
97
+ return pluginId.ok && nonEmpty(identity.subject) ? ok({ identity: { kind: "external", key: key.value, pluginId: pluginId.value, subject: identity.subject }, presentation, evidence: value.evidence }) : err("invalid-input", "Invalid external editor identity.");
98
+ }
99
+ return err("invalid-input", "Invalid editor identity.");
100
+ }
101
+ function decodeEnvelope(value, codec) {
102
+ if (!isRecord(value) || typeof value.schemaVersion !== "number" || !Number.isInteger(value.schemaVersion) || value.schemaVersion < 0) {
103
+ return err("invalid-input", "Invalid state envelope.");
104
+ }
105
+ const address = decodeAddress(value.address);
106
+ const version = decodeVersion(value.version);
107
+ const decoded = codec.decode(value.value);
108
+ const editor = decodeActor(value.lastEditedBy);
109
+ return address.ok && version.ok && decoded.ok && editor.ok ? ok({ address: address.value, version: version.value, schemaVersion: value.schemaVersion, value: decoded.value, lastEditedBy: editor.value }) : err("invalid-input", "Invalid state envelope fields.");
110
+ }
111
+ function decodeRead(value, codec) {
112
+ if (!isRecord(value) || typeof value.status !== "string")
113
+ return err("invalid-input", "Invalid state read.");
114
+ if (value.status === "present") {
115
+ const envelope = decodeEnvelope(value.envelope, codec);
116
+ return envelope.ok ? ok({ status: "present", envelope: envelope.value }) : envelope;
117
+ }
118
+ if (value.status === "empty") {
119
+ const address = decodeAddress(value.address);
120
+ const version = decodeVersion(value.version);
121
+ return address.ok && version.ok ? ok({ status: "empty", address: address.value, version: version.value }) : err("invalid-input", "Invalid empty state read.");
122
+ }
123
+ if (value.status === "migration-required") {
124
+ const address = decodeAddress(value.address);
125
+ return address.ok && typeof value.storedSchemaVersion === "number" && Number.isInteger(value.storedSchemaVersion) && value.storedSchemaVersion >= 0 ? ok({ status: "migration-required", address: address.value, storedSchemaVersion: value.storedSchemaVersion }) : err("invalid-input", "Invalid migration-required read.");
126
+ }
127
+ return err("invalid-input", "Unknown state read status.");
128
+ }
129
+ function decodeMutation(value, codec) {
130
+ if (!isRecord(value) || value.kind !== "initialize" && value.kind !== "replace" || !isRecord(value.expected) || typeof value.schemaVersion !== "number" || !Number.isInteger(value.schemaVersion) || value.schemaVersion < 0 || typeof value.localGeneration !== "number" || !Number.isInteger(value.localGeneration) || value.localGeneration < 0) {
131
+ return err("invalid-input", "Invalid state mutation.");
132
+ }
133
+ const address = decodeAddress(value.address);
134
+ const expectedVersion = decodeVersion(value.expectedVersion);
135
+ const actor = identityKeyCodec.decode(value.expected.actor);
136
+ const session = idCodec("server-session").decode(value.expected.session);
137
+ const ownerSession = idCodec("owner-session").decode(value.ownerSession);
138
+ const operationId = idCodec("operation").decode(value.operationId);
139
+ const decoded = codec.decode(value.value);
140
+ return address.ok && expectedVersion.ok && actor.ok && session.ok && ownerSession.ok && operationId.ok && decoded.ok ? ok({
141
+ kind: value.kind,
142
+ address: address.value,
143
+ expectedVersion: expectedVersion.value,
144
+ expected: { actor: actor.value, session: session.value },
145
+ ownerSession: ownerSession.value,
146
+ localGeneration: value.localGeneration,
147
+ operationId: operationId.value,
148
+ schemaVersion: value.schemaVersion,
149
+ value: decoded.value
150
+ }) : err("invalid-input", "Invalid state mutation fields.");
151
+ }
152
+ function decodeOutcome(value, codec) {
153
+ if (!isRecord(value) || typeof value.status !== "string")
154
+ return err("invalid-input", "Invalid state outcome.");
155
+ const operationId = idCodec("operation").decode(value.operationId);
156
+ if (!operationId.ok)
157
+ return operationId;
158
+ if (value.status === "saved" || value.status === "unchanged") {
159
+ const envelope = decodeEnvelope(value.envelope, codec);
160
+ return envelope.ok ? ok({ status: value.status, envelope: envelope.value, operationId: operationId.value }) : envelope;
161
+ }
162
+ if (value.status === "already-initialized" || value.status === "conflict") {
163
+ const current = decodeRead(value.current, codec);
164
+ return current.ok ? ok({ status: value.status, current: current.value, operationId: operationId.value }) : current;
165
+ }
166
+ return err("invalid-input", "Unknown state outcome status.");
167
+ }
168
+ function decodeSave(value, codec) {
169
+ if (isRecord(value) && value.status === "indeterminate") {
170
+ const operationId = idCodec("operation").decode(value.operationId);
171
+ return operationId.ok ? ok({ status: "indeterminate", operationId: operationId.value }) : operationId;
172
+ }
173
+ return decodeOutcome(value, codec);
174
+ }
175
+ function stateAddressKey(address) {
176
+ return JSON.stringify([address.instanceId, address.pluginId, address.collection, address.recordId, address.owner]);
177
+ }
178
+ function decodeLookup(value, codec) {
179
+ if (!isRecord(value) || typeof value.status !== "string")
180
+ return err("invalid-input", "Invalid state lookup.");
181
+ if (value.status === "final") {
182
+ const outcome = decodeOutcome(value.outcome, codec);
183
+ return outcome.ok ? ok({ status: "final", outcome: outcome.value }) : outcome;
184
+ }
185
+ if (value.status === "pending")
186
+ return ok({ status: "pending" });
187
+ if (value.status === "absent-final" && value.retry === "same-operation-only")
188
+ return ok({ status: "absent-final", retry: "same-operation-only" });
189
+ if (value.status === "unknown" && (value.reason === "unsupported" || value.reason === "expired" || value.reason === "unavailable"))
190
+ return ok({ status: "unknown", reason: value.reason });
191
+ return err("invalid-input", "Invalid state lookup metadata.");
192
+ }
193
+ function encodeAddress(address) {
194
+ return { instanceId: address.instanceId, pluginId: address.pluginId, collection: address.collection, recordId: address.recordId, owner: address.owner };
195
+ }
196
+ function encodeVersion(version) {
197
+ return { epoch: version.epoch, sequence: version.sequence };
198
+ }
199
+ function encodeActor(actor) {
200
+ return actor === null ? null : { identity: actor.identity, presentation: actor.presentation, evidence: actor.evidence };
201
+ }
202
+ function encodeEnvelope(envelope, codec) {
203
+ return { address: encodeAddress(envelope.address), version: encodeVersion(envelope.version), schemaVersion: envelope.schemaVersion, value: codec.encode(envelope.value), lastEditedBy: encodeActor(envelope.lastEditedBy) };
204
+ }
205
+ function encodeRead(read, codec) {
206
+ return read.status === "present" ? { status: "present", envelope: encodeEnvelope(read.envelope, codec) } : read.status === "empty" ? { status: "empty", address: encodeAddress(read.address), version: encodeVersion(read.version) } : read.status === "migration-required" ? { status: "migration-required", address: encodeAddress(read.address), storedSchemaVersion: read.storedSchemaVersion } : (() => {
207
+ throw new TypeError("Invalid state read status.");
208
+ })();
209
+ }
210
+ function encodeMutation(mutation, codec) {
211
+ return { ...mutation, address: encodeAddress(mutation.address), expectedVersion: encodeVersion(mutation.expectedVersion), value: codec.encode(mutation.value) };
212
+ }
213
+ function encodeOutcome(outcome, codec) {
214
+ if (outcome.status === "saved" || outcome.status === "unchanged") {
215
+ return { status: outcome.status, envelope: encodeEnvelope(outcome.envelope, codec), operationId: outcome.operationId };
216
+ }
217
+ if ("current" in outcome)
218
+ return { status: outcome.status, current: encodeRead(outcome.current, codec), operationId: outcome.operationId };
219
+ throw new TypeError("Invalid state outcome.");
220
+ }
221
+ function encodeSave(save, codec) {
222
+ return save.status === "indeterminate" ? { status: "indeterminate", operationId: save.operationId } : encodeOutcome(save, codec);
223
+ }
224
+ function stateCodecs(value) {
225
+ return {
226
+ read: { decode: (input) => decodeRead(input, value), encode: (input) => encodeRead(input, value) },
227
+ mutation: { decode: (input) => decodeMutation(input, value), encode: (input) => encodeMutation(input, value) },
228
+ save: { decode: (input) => decodeSave(input, value), encode: (input) => encodeSave(input, value) },
229
+ invalidation: {
230
+ decode(input) {
231
+ if (!isRecord(input))
232
+ return err("invalid-input", "Invalid state invalidation.");
233
+ const address = decodeAddress(input.address);
234
+ const version = decodeVersion(input.version);
235
+ const operationId = input.operationId === null ? ok(null) : idCodec("operation").decode(input.operationId);
236
+ return address.ok && version.ok && operationId.ok ? ok({ address: address.value, version: version.value, operationId: operationId.value }) : err("invalid-input", "Invalid state invalidation fields.");
237
+ },
238
+ encode: (input) => ({ address: encodeAddress(input.address), version: encodeVersion(input.version), operationId: input.operationId })
239
+ },
240
+ lookup: {
241
+ decode: (input) => decodeLookup(input, value),
242
+ encode: (input) => input.status === "final" ? { status: "final", outcome: encodeOutcome(input.outcome, value) } : input
243
+ }
244
+ };
245
+ }
246
+
247
+ // state-controller-runtime.ts
248
+ var defaultScheduler = { now: Date.now, schedule(delay, callback) {
249
+ const timer = setTimeout(callback, delay);
250
+ return () => clearTimeout(timer);
251
+ } };
252
+ function frozen(value) {
253
+ if (value !== null && typeof value === "object") {
254
+ for (const child of Object.values(value))
255
+ frozen(child);
256
+ Object.freeze(value);
257
+ }
258
+ return value;
259
+ }
260
+ function valueClone(codec, value) {
261
+ const result = codec.decode(structuredClone(codec.encode(value)));
262
+ if (!result.ok)
263
+ throw new TypeError(result.error.message);
264
+ return frozen(result.value);
265
+ }
266
+ function version(read) {
267
+ return read.status === "present" ? read.envelope.version : read.version;
268
+ }
269
+ function sameVersion(a, b) {
270
+ return a.status === b.status && version(a).epoch === version(b).epoch && version(a).sequence === version(b).sequence;
271
+ }
272
+ function createIdentityState(options) {
273
+ const address = frozen({ ...options.address });
274
+ const expected = frozen({ ...options.expected });
275
+ const definition = options.definition;
276
+ const codec = definition.codec;
277
+ const codecs = stateCodecs(codec);
278
+ const drafts = options.drafts;
279
+ const initial = valueClone(codec, definition.initialValue());
280
+ const scheduler = options.scheduler ?? defaultScheduler;
281
+ const retryLimit = options.retryLimit ?? 3;
282
+ const debounce = options.debounceMs ?? 300;
283
+ if (!Number.isSafeInteger(retryLimit) || retryLimit < 0 || !Number.isFinite(debounce) || debounce < 0)
284
+ throw new TypeError("Invalid state scheduling limits");
285
+ let lifecycle = "new";
286
+ let epoch = 0;
287
+ let loadId = 0;
288
+ let localGeneration = 0;
289
+ let base = null;
290
+ let desired = null;
291
+ let blocked = null;
292
+ let pending = null;
293
+ let networkActive = false;
294
+ let acknowledgedGeneration = -1;
295
+ let activeSave = null;
296
+ let startWork = null;
297
+ let activeLoad = null;
298
+ let timer = null;
299
+ let subscription = null;
300
+ let remotePending = false;
301
+ let remoteSerial = 0;
302
+ let remoteHint = null;
303
+ let closing = false;
304
+ let checkpoint = null;
305
+ let recoverySource = null;
306
+ let automaticSuspensions = 0;
307
+ let automaticEpoch = 0;
308
+ let persistQueue = Promise.resolve();
309
+ let persistSerial = 0;
310
+ const abort = new AbortController;
311
+ const listeners = new Set;
312
+ let snapshot = { status: "loading", address };
313
+ const currentValue = (read) => read.status === "present" ? read.envelope.value : initial;
314
+ const dirty = () => base !== null && desired !== null && !definition.equal(currentValue(base), desired.value);
315
+ const needsSave = () => dirty() || base?.status === "empty" && options.initializeEmpty;
316
+ const active = (captured = epoch) => lifecycle === "active" && captured === epoch;
317
+ const clearTimer = () => {
318
+ timer?.();
319
+ timer = null;
320
+ };
321
+ const draft = () => base === null || desired === null ? null : frozen({ formatVersion: 1, key: { address, actor: expected.actor, ownerSession: options.ownerSession }, schemaVersion: definition.schemaVersion, acknowledged: base, desired: desired.value, localGeneration, inFlight: pending?.mutation ?? null });
322
+ function emit() {
323
+ if (lifecycle === "detached" || lifecycle === "disposed")
324
+ snapshot = { status: "detached", address, draft: draft() };
325
+ else if (blocked)
326
+ snapshot = { status: "blocked", address, ...blocked, draft: draft() };
327
+ else if (base && desired)
328
+ snapshot = { status: "ready", address, ownerSession: options.ownerSession, acknowledged: base, desired: desired.value, dirty: dirty(), saving: networkActive, localGeneration, remotePending };
329
+ else
330
+ snapshot = { status: "loading", address };
331
+ for (const listener of listeners) {
332
+ try {
333
+ listener();
334
+ } catch {}
335
+ }
336
+ }
337
+ function block(reason, message, conflict2 = null) {
338
+ clearTimer();
339
+ blocked = { reason, message, conflict: conflict2 };
340
+ emit();
341
+ }
342
+ function sessionCheck() {
343
+ if (!active())
344
+ return err("disposed", "This state controller is no longer active.");
345
+ const result = options.currentSession();
346
+ if (!result.ok) {
347
+ block("identity-unavailable", result.error.message);
348
+ return result;
349
+ }
350
+ const session = result.value;
351
+ if (session.status !== "ready") {
352
+ block("identity-unavailable", session.error.message);
353
+ return { ok: false, error: session.error };
354
+ }
355
+ if (session.instanceId !== address.instanceId || session.actor.identity.key !== expected.actor || session.stamp !== expected.session) {
356
+ block("owner-changed", "The identity session changed.");
357
+ return err("stale-context", "The identity session changed.", "after-refresh");
358
+ }
359
+ return ok(undefined);
360
+ }
361
+ async function safe(run) {
362
+ try {
363
+ return await run();
364
+ } catch (error) {
365
+ return err("unavailable", error instanceof Error ? error.message : "State operation failed.", "after-reconnect");
366
+ }
367
+ }
368
+ function persist(captured = draft()) {
369
+ if (!drafts)
370
+ return Promise.resolve(ok(null));
371
+ if (!captured)
372
+ return Promise.resolve(ok(null));
373
+ const capturedEpoch = epoch;
374
+ const serial = ++persistSerial;
375
+ const work = persistQueue.then(async () => {
376
+ const result = await safe(() => drafts.write(captured, checkpoint?.revision ?? null));
377
+ if (result.ok)
378
+ checkpoint = result.value;
379
+ else if (active(capturedEpoch) && serial === persistSerial)
380
+ block("storage-error", result.error.message);
381
+ return result;
382
+ });
383
+ persistQueue = work.then(() => {
384
+ return;
385
+ });
386
+ return work;
387
+ }
388
+ function schedule() {
389
+ clearTimer();
390
+ if (!active() || automaticSuspensions || closing || blocked || pending || !needsSave())
391
+ return;
392
+ timer = scheduler.schedule(debounce, () => {
393
+ timer = null;
394
+ sendNew(true);
395
+ });
396
+ }
397
+ function readClone(read) {
398
+ const decoded = codecs.read.decode(structuredClone(codecs.read.encode(read)));
399
+ if (!decoded.ok)
400
+ return decoded;
401
+ const value = decoded.value;
402
+ const returnedAddress = value.status === "present" ? value.envelope.address : value.address;
403
+ if (stateAddressKey(returnedAddress) !== stateAddressKey(address) || value.status === "present" && value.envelope.schemaVersion !== definition.schemaVersion)
404
+ return err("incompatible", "State response does not match this resource.");
405
+ return ok(frozen(value));
406
+ }
407
+ async function applyDecision(context, decision) {
408
+ if (!active() || blocked?.conflict?.token !== context.token)
409
+ return err("stale-context", "Conflict decision was superseded.");
410
+ if (decision.kind === "needs-review") {
411
+ block("conflict", decision.reason, context);
412
+ return ok(undefined);
413
+ }
414
+ if (context.remote.status === "migration-required") {
415
+ block("migration-required", "State migration is required.");
416
+ return err("conflict", "State migration is required.");
417
+ }
418
+ base = context.remote;
419
+ desired = { value: valueClone(codec, decision.kind === "rebase" ? decision.value : currentValue(base)) };
420
+ localGeneration++;
421
+ blocked = null;
422
+ emit();
423
+ const capturedEpoch = epoch;
424
+ const saved = await persist();
425
+ if (!saved.ok)
426
+ return saved;
427
+ if (active(capturedEpoch))
428
+ schedule();
429
+ return ok(undefined);
430
+ }
431
+ async function conflict(cause, remote, original) {
432
+ if (!desired)
433
+ return err("unavailable", "State has no local value.");
434
+ const context = frozen({ cause, token: newConflictToken(), ownerSession: options.ownerSession, base: original, local: desired.value, remote });
435
+ block("conflict", "Remote state conflicts with local intent.", context);
436
+ try {
437
+ return await applyDecision(context, options.onConflict(context));
438
+ } catch (error) {
439
+ if (blocked?.conflict?.token !== context.token)
440
+ return err("stale-context", "Conflict decision was superseded.");
441
+ block("conflict", error instanceof Error ? error.message : "Conflict policy failed.", context);
442
+ return err("conflict", "Conflict policy failed.");
443
+ }
444
+ }
445
+ async function applyRemote(read, cause) {
446
+ if (read.status === "migration-required") {
447
+ block("migration-required", "State migration is required.");
448
+ return err("conflict", "State migration is required.");
449
+ }
450
+ if (pending) {
451
+ remotePending = true;
452
+ emit();
453
+ return ok(undefined);
454
+ }
455
+ if (base && version(base).epoch === version(read).epoch && version(read).sequence < version(base).sequence)
456
+ return ok(undefined);
457
+ if (base && dirty()) {
458
+ if (sameVersion(base, read)) {
459
+ if (!blocked?.conflict)
460
+ blocked = null;
461
+ emit();
462
+ schedule();
463
+ return ok(undefined);
464
+ }
465
+ return conflict(cause, read, base);
466
+ }
467
+ base = read;
468
+ desired = { value: valueClone(codec, currentValue(read)) };
469
+ blocked = null;
470
+ emit();
471
+ schedule();
472
+ return ok(undefined);
473
+ }
474
+ function queueRemote() {
475
+ if (!active() || activeLoad || pending)
476
+ return;
477
+ reload("reconnect");
478
+ }
479
+ function reload(cause) {
480
+ const valid = sessionCheck();
481
+ if (!valid.ok)
482
+ return Promise.resolve(valid);
483
+ const capturedEpoch = epoch;
484
+ const id = ++loadId;
485
+ const serial = remoteSerial;
486
+ remotePending = false;
487
+ const work = (async () => {
488
+ const loaded = await safe(() => options.transport.load({ address, expected }, { signal: abort.signal }));
489
+ if (!active(capturedEpoch) || id !== loadId || serial !== remoteSerial)
490
+ return err("cancelled", "State load was superseded.");
491
+ const live = sessionCheck();
492
+ if (!live.ok)
493
+ return live;
494
+ if (!loaded.ok) {
495
+ block("storage-error", loaded.error.message);
496
+ return loaded;
497
+ }
498
+ const parsed = readClone(loaded.value);
499
+ if (!parsed.ok) {
500
+ block("storage-error", parsed.error.message);
501
+ return parsed;
502
+ }
503
+ return applyRemote(parsed.value, cause);
504
+ })();
505
+ activeLoad = work;
506
+ work.then(() => {
507
+ if (activeLoad === work) {
508
+ activeLoad = null;
509
+ if (active(capturedEpoch) && remoteSerial !== serial)
510
+ queueRemote();
511
+ }
512
+ });
513
+ return work;
514
+ }
515
+ function outcomeCheck(outcome, operationId) {
516
+ const parsed = codecs.save.decode(structuredClone(codecs.save.encode(outcome)));
517
+ if (!parsed.ok)
518
+ return parsed;
519
+ if (parsed.value.operationId !== operationId || parsed.value.status === "indeterminate")
520
+ return err("incompatible", "State receipt has a different operation.");
521
+ const read = "envelope" in parsed.value ? { status: "present", envelope: parsed.value.envelope } : parsed.value.current;
522
+ const checked = readClone(read);
523
+ return checked.ok ? ok(frozen(parsed.value)) : checked;
524
+ }
525
+ async function applyOutcome(captured, raw) {
526
+ const checked = outcomeCheck(raw, captured.mutation.operationId);
527
+ if (!checked.ok) {
528
+ block("indeterminate", checked.error.message);
529
+ return checked;
530
+ }
531
+ const outcome = checked.value;
532
+ pending = null;
533
+ loadId++;
534
+ blocked = null;
535
+ if (outcome.status === "saved" || outcome.status === "unchanged") {
536
+ base = { status: "present", envelope: outcome.envelope };
537
+ acknowledgedGeneration = Math.max(acknowledgedGeneration, captured.generation);
538
+ if (localGeneration === captured.generation)
539
+ desired = { value: valueClone(codec, outcome.envelope.value) };
540
+ emit();
541
+ } else if ("current" in outcome) {
542
+ if (outcome.current.status === "migration-required") {
543
+ block("migration-required", "State migration is required.");
544
+ return err("conflict", "State migration is required.");
545
+ }
546
+ if (outcome.status === "already-initialized" && localGeneration === captured.generation) {
547
+ base = outcome.current;
548
+ desired = { value: valueClone(codec, currentValue(base)) };
549
+ acknowledgedGeneration = captured.generation;
550
+ emit();
551
+ } else {
552
+ const result = await conflict("save-conflict", outcome.current, captured.base);
553
+ if (!result.ok)
554
+ return result;
555
+ }
556
+ }
557
+ const capturedEpoch = epoch;
558
+ const saved = await persist();
559
+ if (!saved.ok)
560
+ return saved;
561
+ if (active(capturedEpoch)) {
562
+ if (remotePending)
563
+ return reload("reconnect");
564
+ schedule();
565
+ }
566
+ return ok(undefined);
567
+ }
568
+ function transmit(captured, automatic = false) {
569
+ const capturedEpoch = epoch;
570
+ const dispatchEpoch = automaticEpoch;
571
+ let suppressed = false;
572
+ const work = (async () => {
573
+ networkActive = true;
574
+ emit();
575
+ const persisted = await persist();
576
+ if (!active(capturedEpoch))
577
+ return err("cancelled", "State save was detached.");
578
+ if (!persisted.ok) {
579
+ pending = null;
580
+ networkActive = false;
581
+ emit();
582
+ return persisted;
583
+ }
584
+ if (automatic && (automaticSuspensions || automaticEpoch !== dispatchEpoch)) {
585
+ suppressed = true;
586
+ if (pending === captured)
587
+ pending = null;
588
+ networkActive = false;
589
+ emit();
590
+ const saved = await persist();
591
+ return saved.ok ? ok(undefined) : saved;
592
+ }
593
+ const valid = sessionCheck();
594
+ if (!valid.ok) {
595
+ networkActive = false;
596
+ return valid;
597
+ }
598
+ const response = await safe(() => options.transport.save(captured.mutation, { signal: abort.signal }));
599
+ if (!active(capturedEpoch) || pending !== captured)
600
+ return err("cancelled", "State save was detached.");
601
+ networkActive = false;
602
+ const live = sessionCheck();
603
+ if (!live.ok)
604
+ return live;
605
+ if (!response.ok) {
606
+ block("indeterminate", response.error.message);
607
+ return response;
608
+ }
609
+ if (response.value.operationId !== captured.mutation.operationId) {
610
+ block("indeterminate", "State operation identity changed.");
611
+ return err("incompatible", "State operation identity changed.");
612
+ }
613
+ if (response.value.status === "indeterminate") {
614
+ block("indeterminate", "State save outcome is unknown.");
615
+ return err("unavailable", "State save outcome is unknown.", "same-operation");
616
+ }
617
+ return applyOutcome(captured, response.value);
618
+ })();
619
+ activeSave = work;
620
+ work.then(() => {
621
+ if (activeSave === work)
622
+ activeSave = null;
623
+ if (suppressed)
624
+ schedule();
625
+ });
626
+ return work;
627
+ }
628
+ function sendNew(automatic = false) {
629
+ if (automatic && automaticSuspensions)
630
+ return Promise.resolve(ok(undefined));
631
+ const valid = sessionCheck();
632
+ if (!valid.ok)
633
+ return Promise.resolve(valid);
634
+ if (blocked)
635
+ return Promise.resolve(err("conflict", blocked.message));
636
+ if (activeSave)
637
+ return activeSave;
638
+ if (pending)
639
+ return Promise.resolve(err("unavailable", "An earlier save needs reconciliation.", "same-operation"));
640
+ if (!base || !desired || !needsSave())
641
+ return Promise.resolve(ok(undefined));
642
+ const mutation = frozen({ kind: base.status === "empty" ? "initialize" : "replace", address, expectedVersion: { ...version(base) }, expected, ownerSession: options.ownerSession, localGeneration, operationId: newOperationId(), schemaVersion: definition.schemaVersion, value: valueClone(codec, desired.value) });
643
+ pending = { mutation, base, generation: localGeneration };
644
+ return transmit(pending, automatic);
645
+ }
646
+ let activeReconcile = null;
647
+ function reconcile(allowReplay) {
648
+ if (activeReconcile)
649
+ return activeReconcile;
650
+ const work = performReconcile(allowReplay);
651
+ activeReconcile = work;
652
+ const clear = () => {
653
+ if (activeReconcile === work)
654
+ activeReconcile = null;
655
+ };
656
+ work.then(clear, clear);
657
+ return work;
658
+ }
659
+ async function performReconcile(allowReplay) {
660
+ const captured = pending;
661
+ if (!captured)
662
+ return ok(undefined);
663
+ const capturedEpoch = epoch;
664
+ const valid = sessionCheck();
665
+ if (!valid.ok)
666
+ return valid;
667
+ const result = await safe(() => options.transport.reconcile({ address, expected, operationId: captured.mutation.operationId }, { signal: abort.signal }));
668
+ if (!active(capturedEpoch) || pending !== captured)
669
+ return err("cancelled", "State reconciliation was superseded.");
670
+ const live = sessionCheck();
671
+ if (!live.ok)
672
+ return live;
673
+ if (!result.ok) {
674
+ block("indeterminate", result.error.message);
675
+ return result;
676
+ }
677
+ if (result.value.status === "final")
678
+ return applyOutcome(captured, result.value.outcome);
679
+ if (result.value.status === "absent-final" && allowReplay) {
680
+ if (captured.mutation.expected.actor !== expected.actor || captured.mutation.expected.session !== expected.session) {
681
+ block("draft-recovery", "The original operation cannot be re-authored under a new session.");
682
+ return err("stale-context", "The original operation requires its original session.");
683
+ }
684
+ blocked = null;
685
+ return transmit(captured);
686
+ }
687
+ block("indeterminate", "The previous save remains unresolved; no replacement was sent.");
688
+ return err("unavailable", "The previous save remains unresolved.", "same-operation");
689
+ }
690
+ async function performFlush() {
691
+ clearTimer();
692
+ const valid = sessionCheck();
693
+ if (!valid.ok)
694
+ return valid;
695
+ const goal = localGeneration;
696
+ let retries = 0;
697
+ if (activeSave) {
698
+ const result = await activeSave;
699
+ if (!result.ok)
700
+ return result;
701
+ }
702
+ while (active()) {
703
+ if (pending) {
704
+ const result2 = await reconcile(retries++ < retryLimit);
705
+ if (!result2.ok)
706
+ return result2;
707
+ }
708
+ if (blocked)
709
+ return err("conflict", blocked.message);
710
+ if (!needsSave() || acknowledgedGeneration >= goal)
711
+ return ok(undefined);
712
+ const result = await sendNew();
713
+ if (!result.ok)
714
+ return result;
715
+ }
716
+ return err("disposed", "The state controller is detached.");
717
+ }
718
+ async function flush() {
719
+ let cancel = () => {};
720
+ const timeout = new Promise((resolve) => {
721
+ cancel = scheduler.schedule(30000, () => {
722
+ if (active())
723
+ block("indeterminate", "Flush timed out; pending intent is retained.");
724
+ resolve(err("unavailable", "Flush timed out; pending intent is retained.", "same-operation"));
725
+ });
726
+ });
727
+ try {
728
+ return await Promise.race([performFlush(), timeout]);
729
+ } finally {
730
+ cancel();
731
+ }
732
+ }
733
+ function detach() {
734
+ if (lifecycle === "detached" || lifecycle === "disposed")
735
+ return draft();
736
+ lifecycle = "detached";
737
+ epoch++;
738
+ loadId++;
739
+ clearTimer();
740
+ abort.abort();
741
+ subscription?.();
742
+ subscription = null;
743
+ emit();
744
+ return draft();
745
+ }
746
+ return {
747
+ getSnapshot: () => snapshot,
748
+ subscribe(listener) {
749
+ if (lifecycle === "disposed")
750
+ return () => {};
751
+ listeners.add(listener);
752
+ return () => listeners.delete(listener);
753
+ },
754
+ start() {
755
+ if (lifecycle === "detached" || lifecycle === "disposed")
756
+ return Promise.resolve(err("disposed", "Create a new state controller after detach."));
757
+ if (startWork)
758
+ return startWork;
759
+ lifecycle = "active";
760
+ subscription = options.transport.subscribe(address, (event) => {
761
+ if (!active() || stateAddressKey(event.address) !== stateAddressKey(address))
762
+ return;
763
+ if (base && event.version.epoch === version(base).epoch && event.version.sequence <= version(base).sequence)
764
+ return;
765
+ if (remoteHint && event.version.epoch === remoteHint.epoch && event.version.sequence <= remoteHint.sequence)
766
+ return;
767
+ remoteHint = { ...event.version };
768
+ remotePending = true;
769
+ remoteSerial++;
770
+ if (blocked?.conflict)
771
+ block("conflict", "Refreshing the remote state before reviewing this conflict.");
772
+ else
773
+ emit();
774
+ queueRemote();
775
+ });
776
+ startWork = reload("dirty-load");
777
+ return startWork;
778
+ },
779
+ edit(value) {
780
+ const valid = sessionCheck();
781
+ if (!valid.ok)
782
+ return valid;
783
+ if (closing || blocked || snapshot.status !== "ready")
784
+ return err("unavailable", "State is not editable.");
785
+ try {
786
+ desired = { value: valueClone(codec, value) };
787
+ } catch (error) {
788
+ return err("invalid-input", error instanceof Error ? error.message : "Invalid state.");
789
+ }
790
+ localGeneration++;
791
+ emit();
792
+ schedule();
793
+ return ok(undefined);
794
+ },
795
+ suspendAutomaticDispatch() {
796
+ automaticSuspensions++;
797
+ automaticEpoch++;
798
+ clearTimer();
799
+ let released = false;
800
+ return () => {
801
+ if (released)
802
+ return;
803
+ released = true;
804
+ automaticSuspensions--;
805
+ if (!automaticSuspensions)
806
+ schedule();
807
+ };
808
+ },
809
+ flush,
810
+ async reconnect(input) {
811
+ const valid = sessionCheck();
812
+ if (!valid.ok)
813
+ return valid;
814
+ const existing = activeSave ?? activeReconcile ?? activeLoad;
815
+ if (input?.reuseInFlight && existing)
816
+ return existing;
817
+ if (activeSave) {
818
+ const settled = await activeSave;
819
+ if (!settled.ok && !pending)
820
+ return settled;
821
+ }
822
+ if (pending) {
823
+ const result = await reconcile(false);
824
+ if (!result.ok)
825
+ return result;
826
+ }
827
+ return reload("reconnect");
828
+ },
829
+ async resolveConflict(token, decision) {
830
+ const valid = sessionCheck();
831
+ if (!valid.ok)
832
+ return valid;
833
+ const context = blocked?.conflict;
834
+ if (!context || context.token !== token)
835
+ return err("stale-context", "Conflict decision was superseded.");
836
+ return applyDecision(context, decision);
837
+ },
838
+ async recover(candidate) {
839
+ if (!drafts)
840
+ return err("unsupported", "Draft recovery is disabled for this state.");
841
+ const valid = sessionCheck();
842
+ if (!valid.ok)
843
+ return valid;
844
+ if (activeSave || pending || dirty())
845
+ return err("conflict", "Resolve current edits before recovering another draft.");
846
+ const source = candidate.draft;
847
+ if (source.formatVersion !== 1 || source.key.actor !== expected.actor || source.schemaVersion !== definition.schemaVersion || stateAddressKey(source.key.address) !== stateAddressKey(address))
848
+ return err("invalid-input", "Draft does not match this resource and actor.");
849
+ const parsed = readClone(source.acknowledged);
850
+ if (!parsed.ok || parsed.value.status === "migration-required")
851
+ return err("invalid-input", "Draft base is invalid.");
852
+ let mutation = null;
853
+ if (source.inFlight) {
854
+ const result = codecs.mutation.decode(structuredClone(codecs.mutation.encode(source.inFlight)));
855
+ if (!result.ok || stateAddressKey(result.value.address) !== stateAddressKey(address) || result.value.schemaVersion !== definition.schemaVersion || result.value.expected.actor !== expected.actor || result.value.localGeneration > source.localGeneration)
856
+ return err("invalid-input", "Draft operation does not match its owner or generation.");
857
+ mutation = frozen(result.value);
858
+ }
859
+ clearTimer();
860
+ base = parsed.value;
861
+ desired = { value: valueClone(codec, source.desired) };
862
+ localGeneration = source.localGeneration;
863
+ recoverySource = frozen(structuredClone(candidate));
864
+ checkpoint = source.key.ownerSession === options.ownerSession ? recoverySource : null;
865
+ if (mutation) {
866
+ pending = { mutation, base, generation: mutation.localGeneration };
867
+ block("draft-recovery", "Reconciling the original operation.");
868
+ const result = await reconcile(false);
869
+ if (!result.ok)
870
+ return result;
871
+ }
872
+ const capturedEpoch = epoch;
873
+ const loaded = await reload("draft-recovery");
874
+ if (!loaded.ok || !active(capturedEpoch))
875
+ return loaded;
876
+ return ok(undefined);
877
+ },
878
+ async discardRecovery(candidate) {
879
+ if (!drafts)
880
+ return err("unsupported", "Draft recovery is disabled for this state.");
881
+ if (!active())
882
+ return err("disposed", "This state controller is no longer active.");
883
+ const source = candidate.draft;
884
+ if (source.formatVersion !== 1 || source.key.actor !== expected.actor || source.schemaVersion !== definition.schemaVersion || stateAddressKey(source.key.address) !== stateAddressKey(address)) {
885
+ return err("invalid-input", "Draft does not match this resource and actor.");
886
+ }
887
+ const removed = await safe(() => drafts.remove(source.key, candidate.revision));
888
+ if (!removed.ok || !active())
889
+ return removed.ok ? err("disposed", "This state controller is no longer active.") : removed;
890
+ const sameCheckpoint = (entry) => entry !== null && entry.revision === candidate.revision && entry.draft.key.actor === source.key.actor && entry.draft.key.ownerSession === source.key.ownerSession && stateAddressKey(entry.draft.key.address) === stateAddressKey(source.key.address);
891
+ if (sameCheckpoint(checkpoint))
892
+ checkpoint = null;
893
+ if (sameCheckpoint(recoverySource))
894
+ recoverySource = null;
895
+ return ok(undefined);
896
+ },
897
+ async recoveryCandidates(readOptions) {
898
+ if (readOptions?.signal?.aborted)
899
+ return err("cancelled", "Recovery lookup was cancelled.");
900
+ if (!drafts)
901
+ return ok([]);
902
+ const result = await safe(() => drafts.find(address, expected.actor, 20));
903
+ if (readOptions?.signal?.aborted)
904
+ return err("cancelled", "Recovery lookup was cancelled.");
905
+ return result;
906
+ },
907
+ checkpoint: () => persist(),
908
+ detach,
909
+ async close(input) {
910
+ if (!drafts) {
911
+ detach();
912
+ return ok(undefined);
913
+ }
914
+ if (input.pending === "flush") {
915
+ closing = true;
916
+ const result = await flush();
917
+ const retained2 = detach();
918
+ if (!result.ok) {
919
+ await persist(retained2);
920
+ return result;
921
+ }
922
+ return ok(undefined);
923
+ }
924
+ const retained = detach();
925
+ if (input.pending === "preserve") {
926
+ const result = await persist(retained);
927
+ return result.ok ? ok(undefined) : result;
928
+ }
929
+ await persistQueue;
930
+ const candidates = new Map;
931
+ for (const entry of [checkpoint, recoverySource])
932
+ if (entry)
933
+ candidates.set(JSON.stringify(entry.draft.key), entry);
934
+ for (const entry of candidates.values()) {
935
+ const removed = await safe(() => drafts.remove(entry.draft.key, entry.revision));
936
+ if (!removed.ok)
937
+ return removed;
938
+ }
939
+ return ok(undefined);
940
+ },
941
+ dispose() {
942
+ detach();
943
+ lifecycle = "disposed";
944
+ listeners.clear();
945
+ }
946
+ };
947
+ }
948
+ export {
949
+ createIdentityState
950
+ };