@tangleai/context 0.21.1 → 0.24.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.
@@ -1,107 +1,115 @@
1
- //@ts-check
2
1
  /** A whole-slot ledger adapter. Reload under the shared Web Lock before writes. */
3
2
  const copy = (value) => value === undefined ? undefined : JSON.parse(JSON.stringify(value));
4
-
5
3
  /**
6
4
  * JSON slots must report failed writes by throwing or returning false. A legacy
7
5
  * slot that swallows errors cannot promise durability; status says unverified.
8
6
  * Web Locks serialize tabs using the same slot name. Without them the adapter
9
7
  * explicitly exposes only the four-method, single-writer contract.
10
- * @param {{ read: () => any, write: (data: any) => any, key?: string, reliable?: boolean }} slot
11
- * @param {{ locks?: any, name?: string, singleWriter?: boolean }} [options]
8
+ * @param [options]
12
9
  */
13
10
  export function createSlotLedgerStorage(slot, options = {}) {
14
- const locks = options.locks;
15
- const name = options.name ?? slot.key ?? 'jaren-ai-ledger';
16
- let closed = false;
17
- const active = () => { if (closed) throw new Error('The ledger storage adapter is closed.'); };
18
- let cache = {};
19
- let error = null;
20
- let durable = slot.reliable === true ? 'durable' : 'unverified';
21
- const load = () => {
22
- active();
23
- const raw = slot.read();
24
- if (raw !== null && raw !== undefined && (typeof raw !== 'object' || Array.isArray(raw)))
25
- throw new TypeError('invalid ledger slot: expected a record map');
26
- cache = copy(raw ?? {});
27
- return cache;
28
- };
29
- const publish = (next) => {
30
- active();
31
- try {
32
- if (slot.write(next) === false) throw new Error('ledger storage write failed');
33
- cache = copy(next);
34
- error = null;
35
- durable = slot.reliable === true ? 'durable' : 'unverified';
36
- }
37
- catch (cause) {
38
- error = cause instanceof Error ? cause.message : String(cause);
39
- durable = 'failed';
40
- throw cause;
41
- }
42
- };
43
- const atomic = typeof locks?.request === 'function';
44
- // WebKit's process-local storage snapshots can remain stale even across a
45
- // locked task boundary. Elect one owner there instead of claiming coherence.
46
- const singleWriter = options.singleWriter ?? false;
47
- let ownership;
48
- let releaseOwner;
49
- let ownerRequest;
50
- const own = () => {
51
- active();
52
- if (!atomic || !singleWriter) return Promise.resolve();
53
- ownership ??= new Promise((resolve, reject) => {
54
- const held = new Promise((release) => { releaseOwner = release; });
55
- ownerRequest = locks.request(`ledger-owner:${name}`, { ifAvailable: true }, (lock) => {
56
- if (!lock) {
57
- reject(new Error('single-writer ledger: another tab owns this storage; close that tab before writing here'));
58
- return;
11
+ const locks = options.locks;
12
+ const name = options.name ?? slot.key ?? 'jaren-ai-ledger';
13
+ let closed = false;
14
+ const active = () => { if (closed)
15
+ throw new Error('The ledger storage adapter is closed.'); };
16
+ let cache = {};
17
+ let error = null;
18
+ let durable = slot.reliable === true ? 'durable' : 'unverified';
19
+ const load = () => {
20
+ active();
21
+ const raw = slot.read();
22
+ if (raw !== null && raw !== undefined && (typeof raw !== 'object' || Array.isArray(raw)))
23
+ throw new TypeError('invalid ledger slot: expected a record map');
24
+ cache = copy(raw ?? {});
25
+ return cache;
26
+ };
27
+ const publish = (next) => {
28
+ active();
29
+ try {
30
+ if (slot.write(next) === false)
31
+ throw new Error('ledger storage write failed');
32
+ cache = copy(next);
33
+ error = null;
34
+ durable = slot.reliable === true ? 'durable' : 'unverified';
59
35
  }
60
- resolve();
61
- return held;
62
- });
63
- ownerRequest.catch(reject);
64
- });
65
- ownership.catch(() => { ownership = undefined; });
66
- return ownership;
67
- };
68
- // Firefox publishes localStorage snapshots at task boundaries. Keep the
69
- // lock until that publication completes, and enter a fresh task before read.
70
- const taskBoundary = () => new Promise((resolve) => setTimeout(resolve, 0));
71
- const locked = (fn) => atomic ? locks.request(`ledger:${name}`, async () => {
72
- await taskBoundary();
73
- try { return fn(); }
74
- finally { await taskBoundary(); }
75
- }) : Promise.resolve().then(fn);
76
- const mutate = async (prefix, transform) => {
77
- await own();
78
- return locked(() => {
79
- const matches = (key) => typeof prefix === 'string' ? key.startsWith(prefix)
80
- : (prefix.keys ?? []).includes(key) || (prefix.prefixes ?? []).some((part) => key.startsWith(part));
81
- const current = load();
82
- const scoped = Object.fromEntries(Object.keys(current).sort()
83
- .filter((key) => matches(key)).map((key) => [key, copy(current[key])]));
84
- const outcome = transform(scoped);
85
- if (!outcome || typeof outcome.then === 'function') throw new TypeError('mutate callback must be synchronous');
86
- if (outcome.next !== undefined) {
87
- const next = copy(outcome.next);
88
- if (Object.keys(next).some((key) => !matches(key))) throw new TypeError('mutation escaped its namespace');
89
- for (const key of Object.keys(current)) if (matches(key)) delete current[key];
90
- publish({ ...current, ...next });
91
- }
92
- return outcome.result;
93
- });
94
- };
95
- return {
96
- ...(atomic ? { mutate } : {}),
97
- status: () => ({ concurrency: atomic && !singleWriter ? 'atomic' : 'single-writer', durability: durable, error }),
98
- close: async () => { closed = true; releaseOwner?.(); await ownerRequest; ownership = undefined; },
99
- get: async (key) => { await own(); return copy(load()[key]); },
100
- set: (key, value) => mutate('', (current) => ({ next: { ...current, [key]: copy(value) } })),
101
- delete: (key) => mutate('', (current) => {
102
- delete current[key];
103
- return { next: current };
104
- }),
105
- keys: async (prefix = '') => { await own(); return Object.keys(load()).filter((key) => key.startsWith(prefix)).sort(); },
106
- };
36
+ catch (cause) {
37
+ error = cause instanceof Error ? cause.message : String(cause);
38
+ durable = 'failed';
39
+ throw cause;
40
+ }
41
+ };
42
+ const atomic = typeof locks?.request === 'function';
43
+ // WebKit's process-local storage snapshots can remain stale even across a
44
+ // locked task boundary. Elect one owner there instead of claiming coherence.
45
+ const singleWriter = options.singleWriter ?? false;
46
+ let ownership;
47
+ let releaseOwner;
48
+ let ownerRequest;
49
+ const own = () => {
50
+ active();
51
+ if (!atomic || !singleWriter)
52
+ return Promise.resolve();
53
+ ownership ??= new Promise((resolve, reject) => {
54
+ const held = new Promise((release) => { releaseOwner = release; });
55
+ ownerRequest = locks.request(`ledger-owner:${name}`, { ifAvailable: true }, (lock) => {
56
+ if (!lock) {
57
+ reject(new Error('single-writer ledger: another tab owns this storage; close that tab before writing here'));
58
+ return;
59
+ }
60
+ resolve();
61
+ return held;
62
+ });
63
+ ownerRequest.catch(reject);
64
+ });
65
+ ownership.catch(() => { ownership = undefined; });
66
+ return ownership;
67
+ };
68
+ // Firefox publishes localStorage snapshots at task boundaries. Keep the
69
+ // lock until that publication completes, and enter a fresh task before read.
70
+ const taskBoundary = () => new Promise((resolve) => setTimeout(resolve, 0));
71
+ const locked = (fn) => atomic ? locks.request(`ledger:${name}`, async () => {
72
+ await taskBoundary();
73
+ try {
74
+ return fn();
75
+ }
76
+ finally {
77
+ await taskBoundary();
78
+ }
79
+ }) : Promise.resolve().then(fn);
80
+ const mutate = async (prefix, transform) => {
81
+ await own();
82
+ return locked(() => {
83
+ const matches = (key) => typeof prefix === 'string' ? key.startsWith(prefix)
84
+ : (prefix.keys ?? []).includes(key) || (prefix.prefixes ?? []).some((part) => key.startsWith(part));
85
+ const current = load();
86
+ const scoped = Object.fromEntries(Object.keys(current).sort()
87
+ .filter((key) => matches(key)).map((key) => [key, copy(current[key])]));
88
+ const outcome = transform(scoped);
89
+ if (!outcome || typeof outcome.then === 'function')
90
+ throw new TypeError('mutate callback must be synchronous');
91
+ if (outcome.next !== undefined) {
92
+ const next = copy(outcome.next);
93
+ if (Object.keys(next).some((key) => !matches(key)))
94
+ throw new TypeError('mutation escaped its namespace');
95
+ for (const key of Object.keys(current))
96
+ if (matches(key))
97
+ delete current[key];
98
+ publish({ ...current, ...next });
99
+ }
100
+ return outcome.result;
101
+ });
102
+ };
103
+ return {
104
+ ...(atomic ? { mutate } : {}),
105
+ status: () => ({ concurrency: atomic && !singleWriter ? 'atomic' : 'single-writer', durability: durable, error }),
106
+ close: async () => { closed = true; releaseOwner?.(); await ownerRequest; ownership = undefined; },
107
+ get: async (key) => { await own(); return copy(load()[key]); },
108
+ set: (key, value) => mutate('', (current) => ({ next: { ...current, [key]: copy(value) } })),
109
+ delete: (key) => mutate('', (current) => {
110
+ delete current[key];
111
+ return { next: current };
112
+ }),
113
+ keys: async (prefix = '') => { await own(); return Object.keys(load()).filter((key) => key.startsWith(prefix)).sort(); },
114
+ };
107
115
  }
@@ -1,32 +1,17 @@
1
+ /** Whether a private view is already enclosed by an atomic publication. */
2
+ export declare const isAtomicView: (view: any) => boolean;
1
3
  /**
2
4
  * Stage async ledger work off-store, then publish through one synchronous CAS.
3
5
  * Readers never observe the staged adapter. Competing commits retry against a
4
6
  * fresh record map; external I/O belongs outside the transform callback.
5
7
  * Four-method adapters retain their explicit single-writer behavior.
6
8
  * @template T
7
- * @param {any} storage
8
- * @param {StorageScope} prefix
9
- * @param {(view: any) => Promise<T>} task
10
- * @returns {Promise<T>}
11
- */
12
- export function atomicTask<T>(storage: any, prefix: StorageScope, task: (view: any) => Promise<T>): Promise<T>;
13
- export function isAtomicView(view: any): boolean;
14
- /**
15
- * Atomically transform the JSON record map under a namespace. The synchronous
16
- * callback receives a detached current map and returns {next, result}; omitting
17
- * next is a read-only decision. Failure publishes nothing. Keys cannot escape
18
- * the namespace. Adapters serialize the callback with every other write.
19
9
  */
10
+ export declare function atomicTask<T>(storage: any, prefix: StorageScope, task: (view: any) => Promise<T>): Promise<T>;
20
11
  export type StorageScope = string | {
21
12
  prefixes?: string[];
22
13
  keys?: string[];
23
14
  };
24
- /**
25
- * Atomically transform the JSON record map under a namespace. The synchronous
26
- * callback receives a detached current map and returns {next, result}; omitting
27
- * next is a read-only decision. Failure publishes nothing. Keys cannot escape
28
- * the namespace. Adapters serialize the callback with every other write.
29
- */
30
15
  export type StorageMutation = (prefix: StorageScope, transform: (current: Record<string, any>) => {
31
16
  next?: Record<string, any>;
32
17
  result?: any;
@@ -1,49 +1,37 @@
1
- //@ts-check
2
- import { createMemoryStorage } from './memory.js';
3
-
4
- /**
5
- * Atomically transform the JSON record map under a namespace. The synchronous
1
+ import { createMemoryStorage } from "./memory.js";
2
+ /** Atomically transform the JSON record map under a namespace. The synchronous
6
3
  * callback receives a detached current map and returns {next, result}; omitting
7
4
  * next is a read-only decision. Failure publishes nothing. Keys cannot escape
8
- * the namespace. Adapters serialize the callback with every other write.
9
- * @typedef {string | { prefixes?: string[], keys?: string[] }} StorageScope
10
- * @typedef {(prefix: StorageScope, transform: (current: Record<string, any>) =>
11
- * { next?: Record<string, any>, result?: any }) => Promise<any>} StorageMutation
12
- */
13
-
5
+ * the namespace. Adapters serialize the callback with every other write. */
14
6
  const staged = new WeakSet();
15
7
  /** Whether a private view is already enclosed by an atomic publication. */
16
8
  export const isAtomicView = (view) => staged.has(view);
17
-
18
9
  const signature = (map) => JSON.stringify(Object.keys(map).sort().map((key) => [key, map[key]]));
19
-
20
10
  /**
21
11
  * Stage async ledger work off-store, then publish through one synchronous CAS.
22
12
  * Readers never observe the staged adapter. Competing commits retry against a
23
13
  * fresh record map; external I/O belongs outside the transform callback.
24
14
  * Four-method adapters retain their explicit single-writer behavior.
25
15
  * @template T
26
- * @param {any} storage
27
- * @param {StorageScope} prefix
28
- * @param {(view: any) => Promise<T>} task
29
- * @returns {Promise<T>}
30
16
  */
31
17
  export async function atomicTask(storage, prefix, task) {
32
- if (typeof storage.mutate !== 'function') return task(storage);
33
- for (let attempt = 0; attempt < 128; attempt++) {
34
- const before = await storage.mutate(prefix, (current) => ({ result: current }));
35
- const expected = signature(before);
36
- const backing = new Map(Object.entries(before).map(([key, value]) => [key, JSON.stringify(value)]));
37
- const view = createMemoryStorage(backing);
38
- // Staging already owns its isolated map; nested transactions need no CAS.
39
- const { mutate: _mutate, ...plain } = view;
40
- staged.add(plain);
41
- const result = await task(plain);
42
- const next = Object.fromEntries([...backing].map(([key, raw]) => [key, JSON.parse(raw)]));
43
- const changed = signature(next) !== expected;
44
- const committed = await storage.mutate(prefix, (current) => signature(current) !== expected
45
- ? { result: false } : { ...(changed ? { next } : {}), result: true });
46
- if (committed) return result;
47
- }
48
- throw Object.assign(new Error('ledger transaction conflict; retry the operation'), { code: 'LEDGER_CONFLICT' });
18
+ if (typeof storage.mutate !== 'function')
19
+ return task(storage);
20
+ for (let attempt = 0; attempt < 128; attempt++) {
21
+ const before = await storage.mutate(prefix, (current) => ({ result: current }));
22
+ const expected = signature(before);
23
+ const backing = new Map(Object.entries(before).map(([key, value]) => [key, JSON.stringify(value)]));
24
+ const view = createMemoryStorage(backing);
25
+ // Staging already owns its isolated map; nested transactions need no CAS.
26
+ const { mutate: _mutate, ...plain } = view;
27
+ staged.add(plain);
28
+ const result = await task(plain);
29
+ const next = Object.fromEntries([...backing].map(([key, raw]) => [key, JSON.parse(raw)]));
30
+ const changed = signature(next) !== expected;
31
+ const committed = await storage.mutate(prefix, (current) => signature(current) !== expected
32
+ ? { result: false } : { ...(changed ? { next } : {}), result: true });
33
+ if (committed)
34
+ return result;
35
+ }
36
+ throw Object.assign(new Error('ledger transaction conflict; retry the operation'), { code: 'LEDGER_CONFLICT' });
49
37
  }