@braidlabs/data 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +41 -0
- package/src/index.d.ts +34 -0
- package/src/index.js +23 -0
- package/src/index.js.map +1 -0
- package/src/lib/adapters.d.ts +74 -0
- package/src/lib/adapters.js +10 -0
- package/src/lib/adapters.js.map +1 -0
- package/src/lib/events.d.ts +141 -0
- package/src/lib/events.js +337 -0
- package/src/lib/events.js.map +1 -0
- package/src/lib/flush.d.ts +49 -0
- package/src/lib/flush.js +60 -0
- package/src/lib/flush.js.map +1 -0
- package/src/lib/indexeddb-driver.d.ts +29 -0
- package/src/lib/indexeddb-driver.js +160 -0
- package/src/lib/indexeddb-driver.js.map +1 -0
- package/src/lib/intents.d.ts +83 -0
- package/src/lib/intents.js +136 -0
- package/src/lib/intents.js.map +1 -0
- package/src/lib/invalidation.d.ts +63 -0
- package/src/lib/invalidation.js +136 -0
- package/src/lib/invalidation.js.map +1 -0
- package/src/lib/locks.d.ts +43 -0
- package/src/lib/locks.js +78 -0
- package/src/lib/locks.js.map +1 -0
- package/src/lib/memory-driver.d.ts +10 -0
- package/src/lib/memory-driver.js +53 -0
- package/src/lib/memory-driver.js.map +1 -0
- package/src/lib/outbox.d.ts +146 -0
- package/src/lib/outbox.js +89 -0
- package/src/lib/outbox.js.map +1 -0
- package/src/lib/partitions.d.ts +70 -0
- package/src/lib/partitions.js +62 -0
- package/src/lib/partitions.js.map +1 -0
- package/src/lib/query.d.ts +235 -0
- package/src/lib/query.js +430 -0
- package/src/lib/query.js.map +1 -0
- package/src/lib/record-store.d.ts +125 -0
- package/src/lib/record-store.js +85 -0
- package/src/lib/record-store.js.map +1 -0
- package/src/lib/tenancy.d.ts +121 -0
- package/src/lib/tenancy.js +220 -0
- package/src/lib/tenancy.js.map +1 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
const PARTITION_INDEX = 'by-partition-seq';
|
|
2
|
+
/**
|
|
3
|
+
* IndexedDB, as the source of truth.
|
|
4
|
+
*
|
|
5
|
+
* Why this rather than a KV shim over the same database: every operation below is **one
|
|
6
|
+
* transaction over one record**. There is no read-modify-write anywhere, so two realms or two tabs
|
|
7
|
+
* writing concurrently cannot lose each other's work — which a `get`-then-`set` shim absolutely can,
|
|
8
|
+
* whatever storage sits underneath it.
|
|
9
|
+
*
|
|
10
|
+
* The `[partition, seq]` compound index is what makes `list` a range scan rather than a full read
|
|
11
|
+
* plus filter, and it is why partitions stay cheap as the store grows.
|
|
12
|
+
*/
|
|
13
|
+
export function indexedDbRecordDriver(options) {
|
|
14
|
+
const databaseName = options.database ?? 'skew-data';
|
|
15
|
+
const factory = options.indexedDB ?? globalThis.indexedDB;
|
|
16
|
+
const collections = [...options.collections];
|
|
17
|
+
const ranges = options.keyRange ?? globalThis.IDBKeyRange;
|
|
18
|
+
let open = null;
|
|
19
|
+
function database() {
|
|
20
|
+
open ??= connect();
|
|
21
|
+
return open;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Opens the database, upgrading only if a declared collection is actually missing.
|
|
25
|
+
*
|
|
26
|
+
* **The version is discovered, never derived.** An earlier draft opened at
|
|
27
|
+
* `collections.length + 1`, which works perfectly for one application and breaks the moment two
|
|
28
|
+
* share an origin — which is exactly what this library is for. Two independently deployed apps
|
|
29
|
+
* rarely declare the same number of collections, so each would demand its own version: the one
|
|
30
|
+
* with the shorter list ends up requesting a version *lower* than the database already has, and
|
|
31
|
+
* IndexedDB answers that with a `VersionError` forever. Not a race, not intermittent — that app
|
|
32
|
+
* simply never opens its storage again.
|
|
33
|
+
*
|
|
34
|
+
* Discovering it instead means the two converge. Each opens at whatever version exists, adds only
|
|
35
|
+
* the stores it is missing, and the version climbs monotonically as fragments arrive.
|
|
36
|
+
*/
|
|
37
|
+
async function connect() {
|
|
38
|
+
if (!factory)
|
|
39
|
+
throw new Error('skew-data: no IndexedDB in this environment');
|
|
40
|
+
let db = await openDatabase();
|
|
41
|
+
const missing = collections.filter((collection) => !db.objectStoreNames.contains(collection));
|
|
42
|
+
if (missing.length > 0) {
|
|
43
|
+
// A connection has to be closed before it can be upgraded, including our own.
|
|
44
|
+
db.close();
|
|
45
|
+
db = await openDatabase(db.version + 1);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Stand aside when another context needs to upgrade.
|
|
49
|
+
*
|
|
50
|
+
* This is the other half of sharing a database, and the half that hangs rather than throws when
|
|
51
|
+
* it is missing: a sibling fragment declaring a collection we do not have must upgrade, and an
|
|
52
|
+
* upgrade cannot start while anyone holds the database open. Closing here lets it proceed; the
|
|
53
|
+
* next operation reopens at the new version, by which point its store exists.
|
|
54
|
+
*/
|
|
55
|
+
db.onversionchange = () => {
|
|
56
|
+
db.close();
|
|
57
|
+
open = null;
|
|
58
|
+
};
|
|
59
|
+
return db;
|
|
60
|
+
}
|
|
61
|
+
function openDatabase(version) {
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
const request = version === undefined ? factory.open(databaseName) : factory.open(databaseName, version);
|
|
64
|
+
request.onupgradeneeded = () => {
|
|
65
|
+
const db = request.result;
|
|
66
|
+
for (const collection of collections) {
|
|
67
|
+
if (db.objectStoreNames.contains(collection))
|
|
68
|
+
continue;
|
|
69
|
+
const store = db.createObjectStore(collection, { keyPath: 'id' });
|
|
70
|
+
store.createIndex(PARTITION_INDEX, ['partition', 'seq']);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
request.onsuccess = () => resolve(request.result);
|
|
74
|
+
request.onerror = () => reject(request.error ?? new Error('skew-data: could not open the database'));
|
|
75
|
+
// Someone is holding the database open at an older version and did not stand aside. Every
|
|
76
|
+
// connection this driver makes closes itself on `versionchange`, so the holder is something
|
|
77
|
+
// else — an old build, another library, or a devtools inspection. Reported rather than left
|
|
78
|
+
// to hang, because a promise that never settles is the least debuggable failure there is.
|
|
79
|
+
request.onblocked = () => reject(new Error(`skew-data: another connection is holding "${databaseName}" open at an older version, ` +
|
|
80
|
+
`so it cannot be upgraded to add ${collections.join(', ')}. Close other tabs of this ` +
|
|
81
|
+
`application, or reload them.`));
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Object stores can only be created during a version upgrade, so a collection that was not
|
|
86
|
+
* declared cannot be created on demand — and IndexedDB reports that as a bare `NotFoundError`
|
|
87
|
+
* naming neither the collection nor the fix. Checking first turns an opaque failure into an
|
|
88
|
+
* actionable one, which is worth the branch.
|
|
89
|
+
*/
|
|
90
|
+
function assertDeclared(db, collection) {
|
|
91
|
+
if (db.objectStoreNames.contains(collection))
|
|
92
|
+
return;
|
|
93
|
+
throw new Error(`skew-data: collection "${collection}" is not in this database. IndexedDB can only create ` +
|
|
94
|
+
`object stores during a version upgrade, so every collection must be declared up front — ` +
|
|
95
|
+
`add it to \`collections\` where the driver is created (currently: ${collections.join(', ') || 'none'}).`);
|
|
96
|
+
}
|
|
97
|
+
async function transaction(collection, mode, run) {
|
|
98
|
+
const db = await database();
|
|
99
|
+
assertDeclared(db, collection);
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
const tx = db.transaction(collection, mode);
|
|
102
|
+
const request = run(tx.objectStore(collection));
|
|
103
|
+
request.onsuccess = () => resolve(request.result);
|
|
104
|
+
request.onerror = () => reject(request.error ?? new Error(`skew-data: ${collection} operation failed`));
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
async put(collection, record) {
|
|
109
|
+
await transaction(collection, 'readwrite', (store) => store.put(record));
|
|
110
|
+
},
|
|
111
|
+
async get(collection, id) {
|
|
112
|
+
return (await transaction(collection, 'readonly', (store) => store.get(id))) ?? null;
|
|
113
|
+
},
|
|
114
|
+
async delete(collection, id) {
|
|
115
|
+
await transaction(collection, 'readwrite', (store) => store.delete(id));
|
|
116
|
+
},
|
|
117
|
+
async list(collection, partition) {
|
|
118
|
+
// Range scan over [partition, seq] — already ordered, so no sort and no full read.
|
|
119
|
+
const range = ranges.bound([partition, -Infinity], [partition, Infinity]);
|
|
120
|
+
return transaction(collection, 'readonly', (store) => store.index(PARTITION_INDEX).getAll(range));
|
|
121
|
+
},
|
|
122
|
+
async clearPartition(collection, partition) {
|
|
123
|
+
const db = await database();
|
|
124
|
+
assertDeclared(db, collection);
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
const tx = db.transaction(collection, 'readwrite');
|
|
127
|
+
const store = tx.objectStore(collection);
|
|
128
|
+
const range = ranges.bound([partition, -Infinity], [partition, Infinity]);
|
|
129
|
+
const cursor = store.index(PARTITION_INDEX).openCursor(range);
|
|
130
|
+
cursor.onsuccess = () => {
|
|
131
|
+
const position = cursor.result;
|
|
132
|
+
if (!position)
|
|
133
|
+
return;
|
|
134
|
+
position.delete();
|
|
135
|
+
position.continue();
|
|
136
|
+
};
|
|
137
|
+
// Resolved on the transaction, not the cursor: deleting through a cursor is only durable
|
|
138
|
+
// once the transaction commits, and a sign-out purge that reported success before that
|
|
139
|
+
// could leave a partition half-deleted.
|
|
140
|
+
tx.oncomplete = () => resolve();
|
|
141
|
+
tx.onerror = () => reject(tx.error ?? new Error('skew-data: purge failed'));
|
|
142
|
+
tx.onabort = () => reject(tx.error ?? new Error('skew-data: purge aborted'));
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
async highestSeq(collection) {
|
|
146
|
+
const db = await database();
|
|
147
|
+
assertDeclared(db, collection);
|
|
148
|
+
return new Promise((resolve, reject) => {
|
|
149
|
+
const tx = db.transaction(collection, 'readonly');
|
|
150
|
+
const cursor = tx.objectStore(collection).index(PARTITION_INDEX).openCursor(null, 'prev');
|
|
151
|
+
cursor.onsuccess = () => {
|
|
152
|
+
const record = cursor.result?.value;
|
|
153
|
+
resolve(record?.seq ?? 0);
|
|
154
|
+
};
|
|
155
|
+
cursor.onerror = () => reject(cursor.error ?? new Error('skew-data: could not read the sequence'));
|
|
156
|
+
});
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=indexeddb-driver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"indexeddb-driver.js","sourceRoot":"","sources":["../../../../../libs/data/src/lib/indexeddb-driver.ts"],"names":[],"mappings":"AAmBA,MAAM,eAAe,GAAG,kBAAkB,CAAC;AAE3C;;;;;;;;;;GAUG;AACH,MAAM,UAAU,qBAAqB,CAAC,OAA+B;IACnE,MAAM,YAAY,GAAG,OAAO,CAAC,QAAQ,IAAI,WAAW,CAAC;IACrD,MAAM,OAAO,GAAG,OAAO,CAAC,SAAS,IAAI,UAAU,CAAC,SAAS,CAAC;IAC1D,MAAM,WAAW,GAAG,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAC7C,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,IAAI,UAAU,CAAC,WAAW,CAAC;IAE1D,IAAI,IAAI,GAAgC,IAAI,CAAC;IAE7C,SAAS,QAAQ;QACf,IAAI,KAAK,OAAO,EAAE,CAAC;QACnB,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;;;;;;;;;;;OAaG;IACH,KAAK,UAAU,OAAO;QACpB,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAC;QAE7E,IAAI,EAAE,GAAG,MAAM,YAAY,EAAE,CAAC;QAC9B,MAAM,OAAO,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,gBAAgB,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC;QAE9F,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvB,8EAA8E;YAC9E,EAAE,CAAC,KAAK,EAAE,CAAC;YACX,EAAE,GAAG,MAAM,YAAY,CAAC,EAAE,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC;QAC1C,CAAC;QAED;;;;;;;WAOG;QACH,EAAE,CAAC,eAAe,GAAG,GAAG,EAAE;YACxB,EAAE,CAAC,KAAK,EAAE,CAAC;YACX,IAAI,GAAG,IAAI,CAAC;QACd,CAAC,CAAC;QAEF,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,SAAS,YAAY,CAAC,OAAgB;QACpC,OAAO,IAAI,OAAO,CAAc,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YAClD,MAAM,OAAO,GAAG,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,OAAQ,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,OAAQ,CAAC,IAAI,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;YAE3G,OAAO,CAAC,eAAe,GAAG,GAAG,EAAE;gBAC7B,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;gBAC1B,KAAK,MAAM,UAAU,IAAI,WAAW,EAAE,CAAC;oBACrC,IAAI,EAAE,CAAC,gBAAgB,CAAC,QAAQ,CAAC,UAAU,CAAC;wBAAE,SAAS;oBACvD,MAAM,KAAK,GAAG,EAAE,CAAC,iBAAiB,CAAC,UAAU,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;oBAClE,KAAK,CAAC,WAAW,CAAC,eAAe,EAAE,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC,CAAC;gBAC3D,CAAC;YACH,CAAC,CAAC;YAEF,OAAO,CAAC,SAAS,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;YAClD,OAAO,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,IAAI,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC,CAAC;YAErG,0FAA0F;YAC1F,4FAA4F;YAC5F,4FAA4F;YAC5F,0FAA0F;YAC1F,OAAO,CAAC,SAAS,GAAG,GAAG,EAAE,CACvB,MAAM,CACJ,IAAI,KAAK,CACP,6CAA6C,YAAY,8BAA8B;gBACrF,mCAAmC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,6BAA6B;gBACtF,8BAA8B,CACjC,CACF,CAAC;QACN,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;;;;OAKG;IACH,SAAS,cAAc,CAAC,EAAe,EAAE,UAAkB;QACzD,IAAI,EAAE,CAAC,gBAAgB,CAAC,QAAQ,CAAC,UAAU,CAAC;YAAE,OAAO;QACrD,MAAM,IAAI,KAAK,CACb,0BAA0B,UAAU,uDAAuD;YACzF,0FAA0F;YAC1F,qEAAqE,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,MAAM,IAAI,CAC5G,CAAC;IACJ,CAAC;IAED,KAAK,UAAU,WAAW,CACxB,UAAkB,EAClB,IAAwB,EACxB,GAA6C;QAE7C,MAAM,EAAE,GAAG,MAAM,QAAQ,EAAE,CAAC;QAC5B,cAAc,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;QAC/B,OAAO,IAAI,OAAO,CAAI,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACxC,MAAM,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;YAC5C,MAAM,OAAO,GAAG,GAAG,CAAC,EAAE,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC,CAAC;YAChD,OAAO,CAAC,SAAS,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;YAClD,OAAO,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,IAAI,IAAI,KAAK,CAAC,cAAc,UAAU,mBAAmB,CAAC,CAAC,CAAC;QAC1G,CAAC,CAAC,CAAC;IACL,CAAC;IAED,OAAO;QACL,KAAK,CAAC,GAAG,CAAC,UAAU,EAAE,MAAM;YAC1B,MAAM,WAAW,CAAC,UAAU,EAAE,WAAW,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;QAC3E,CAAC;QAED,KAAK,CAAC,GAAG,CAAC,UAAU,EAAE,EAAE;YACtB,OAAO,CAAC,MAAM,WAAW,CAA2B,UAAU,EAAE,UAAU,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;QACjH,CAAC;QAED,KAAK,CAAC,MAAM,CAAC,UAAU,EAAE,EAAE;YACzB,MAAM,WAAW,CAAC,UAAU,EAAE,WAAW,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;QAC1E,CAAC;QAED,KAAK,CAAC,IAAI,CAAC,UAAU,EAAE,SAAS;YAC9B,mFAAmF;YACnF,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC,CAAC;YAC1E,OAAO,WAAW,CAAiB,UAAU,EAAE,UAAU,EAAE,CAAC,KAAK,EAAE,EAAE,CACnE,KAAK,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAC3C,CAAC;QACJ,CAAC;QAED,KAAK,CAAC,cAAc,CAAC,UAAU,EAAE,SAAS;YACxC,MAAM,EAAE,GAAG,MAAM,QAAQ,EAAE,CAAC;YAC5B,cAAc,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;YAC/B,OAAO,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;gBAC3C,MAAM,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;gBACnD,MAAM,KAAK,GAAG,EAAE,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC;gBACzC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC,CAAC;gBAC1E,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;gBAE9D,MAAM,CAAC,SAAS,GAAG,GAAG,EAAE;oBACtB,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC;oBAC/B,IAAI,CAAC,QAAQ;wBAAE,OAAO;oBACtB,QAAQ,CAAC,MAAM,EAAE,CAAC;oBAClB,QAAQ,CAAC,QAAQ,EAAE,CAAC;gBACtB,CAAC,CAAC;gBAEF,yFAAyF;gBACzF,uFAAuF;gBACvF,wCAAwC;gBACxC,EAAE,CAAC,UAAU,GAAG,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC;gBAChC,EAAE,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,IAAI,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC,CAAC;gBAC5E,EAAE,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,IAAI,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC,CAAC;YAC/E,CAAC,CAAC,CAAC;QACL,CAAC;QAED,KAAK,CAAC,UAAU,CAAC,UAAU;YACzB,MAAM,EAAE,GAAG,MAAM,QAAQ,EAAE,CAAC;YAC5B,cAAc,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;YAC/B,OAAO,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;gBAC7C,MAAM,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;gBAClD,MAAM,MAAM,GAAG,EAAE,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;gBAE1F,MAAM,CAAC,SAAS,GAAG,GAAG,EAAE;oBACtB,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,KAAiC,CAAC;oBAChE,OAAO,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC;gBAC5B,CAAC,CAAC;gBACF,MAAM,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,IAAI,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC,CAAC;YACrG,CAAC,CAAC,CAAC;QACL,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { VersionedSchema } from '@braidlabs/skew';
|
|
2
|
+
/**
|
|
3
|
+
* Intents: "somebody handle this", and deciding who.
|
|
4
|
+
*
|
|
5
|
+
* The FDC3 shape, with the resolution question answered honestly. FDC3's answer is a resolver UI,
|
|
6
|
+
* which is *one* policy — and the wrong one for most internal software, where two candidates usually
|
|
7
|
+
* means one is a newer deployment of the other and nobody wants to be asked.
|
|
8
|
+
*
|
|
9
|
+
* The part that is ours rather than FDC3's: **a handler that cannot read the payload is not a
|
|
10
|
+
* candidate.** Version eligibility is decided before resolution, not discovered during it, so a user
|
|
11
|
+
* is never offered a handler that will fail — and an automatic policy never picks one.
|
|
12
|
+
*/
|
|
13
|
+
export interface IntentHandlerOptions<T = unknown> {
|
|
14
|
+
/** The contract version this handler accepts. It is excluded if the payload cannot reach it. */
|
|
15
|
+
as?: number;
|
|
16
|
+
/** Ranks candidates for the `'first'` policy and for a resolver. Higher wins; defaults to 0. */
|
|
17
|
+
priority?: number;
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
/** For diagnostics and for a chooser UI. Defaults to the bus's consumer name. */
|
|
20
|
+
label?: string;
|
|
21
|
+
handler: (payload: T) => unknown | Promise<unknown>;
|
|
22
|
+
}
|
|
23
|
+
export interface IntentCandidate {
|
|
24
|
+
intent: string;
|
|
25
|
+
/** Which application registered it. */
|
|
26
|
+
consumer: string;
|
|
27
|
+
label: string;
|
|
28
|
+
priority: number;
|
|
29
|
+
/** The contract version it reads the payload at. */
|
|
30
|
+
as?: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* How to choose between handlers.
|
|
34
|
+
*
|
|
35
|
+
* `'first'` is the default because it is deterministic and it is what an internal deployment wants;
|
|
36
|
+
* `'ask'` is FDC3's behaviour and needs the host to render something, so it arrives as a callback the
|
|
37
|
+
* shell supplies rather than as UI this package owns.
|
|
38
|
+
*/
|
|
39
|
+
export type ResolvePolicy = 'first' | 'all' | 'ask' | ((candidates: readonly IntentCandidate[], payload: unknown) => IntentCandidate | Promise<IntentCandidate>);
|
|
40
|
+
export interface RaiseOptions {
|
|
41
|
+
resolve?: ResolvePolicy;
|
|
42
|
+
/** Required when `resolve` is `'ask'`: the shell's chooser. */
|
|
43
|
+
chooser?: (candidates: readonly IntentCandidate[], payload: unknown) => Promise<IntentCandidate>;
|
|
44
|
+
}
|
|
45
|
+
export interface IntentResult<R = unknown> {
|
|
46
|
+
/** Every handler that ran, in the order it ran. */
|
|
47
|
+
handled: {
|
|
48
|
+
candidate: IntentCandidate;
|
|
49
|
+
result: R;
|
|
50
|
+
}[];
|
|
51
|
+
/** Candidates excluded because they could not read the payload at their declared version. */
|
|
52
|
+
ineligible: {
|
|
53
|
+
candidate: IntentCandidate;
|
|
54
|
+
reason: string;
|
|
55
|
+
}[];
|
|
56
|
+
}
|
|
57
|
+
export declare class NoIntentHandlerError extends Error {
|
|
58
|
+
readonly intent: string;
|
|
59
|
+
readonly ineligible: readonly {
|
|
60
|
+
candidate: IntentCandidate;
|
|
61
|
+
reason: string;
|
|
62
|
+
}[];
|
|
63
|
+
constructor(intent: string, ineligible: readonly {
|
|
64
|
+
candidate: IntentCandidate;
|
|
65
|
+
reason: string;
|
|
66
|
+
}[]);
|
|
67
|
+
}
|
|
68
|
+
export interface IntentRegistry {
|
|
69
|
+
addIntentListener<T>(intent: string, options: IntentHandlerOptions<T>): () => void;
|
|
70
|
+
raiseIntent<R = unknown>(intent: string, payload: unknown, options?: RaiseOptions): Promise<IntentResult<R>>;
|
|
71
|
+
/** Who could handle this intent right now, for a launcher or a chooser. */
|
|
72
|
+
candidates(intent: string, payload?: unknown): {
|
|
73
|
+
eligible: IntentCandidate[];
|
|
74
|
+
ineligible: IntentCandidate[];
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export interface IntentRegistryOptions {
|
|
78
|
+
consumer: string;
|
|
79
|
+
/** Resolves the contract for an intent's payload, so eligibility can be decided. */
|
|
80
|
+
schemaFor: (intent: string) => VersionedSchema<unknown> | undefined;
|
|
81
|
+
onEventError?: (message: string, detail?: unknown) => void;
|
|
82
|
+
}
|
|
83
|
+
export declare function createIntentRegistry(options: IntentRegistryOptions): IntentRegistry;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
export class NoIntentHandlerError extends Error {
|
|
2
|
+
intent;
|
|
3
|
+
ineligible;
|
|
4
|
+
constructor(intent, ineligible) {
|
|
5
|
+
const excluded = ineligible.length
|
|
6
|
+
? ` ${ineligible.length} handler(s) were excluded: ${ineligible.map((entry) => `${entry.candidate.consumer} (${entry.reason})`).join(', ')}`
|
|
7
|
+
: '';
|
|
8
|
+
super(`[skew/data] nothing can handle the intent "${intent}".${excluded}`);
|
|
9
|
+
this.intent = intent;
|
|
10
|
+
this.ineligible = ineligible;
|
|
11
|
+
this.name = 'NoIntentHandlerError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function createIntentRegistry(options) {
|
|
15
|
+
const registrations = new Map();
|
|
16
|
+
/**
|
|
17
|
+
* Whether a handler can be handed this payload.
|
|
18
|
+
*
|
|
19
|
+
* Decided from the declared chain rather than by attempting the projection, so the answer does not
|
|
20
|
+
* depend on a payload being present — `candidates()` has to work for a launcher with nothing in
|
|
21
|
+
* hand.
|
|
22
|
+
*/
|
|
23
|
+
function eligibility(registration) {
|
|
24
|
+
const schema = options.schemaFor(registration.intent);
|
|
25
|
+
if (!schema || registration.as === undefined || registration.as === schema.version)
|
|
26
|
+
return { ok: true };
|
|
27
|
+
if (registration.as > schema.version) {
|
|
28
|
+
return { ok: false, reason: `reads v${registration.as}, the payload is v${schema.version}` };
|
|
29
|
+
}
|
|
30
|
+
const missing = schema.steps
|
|
31
|
+
.filter((step) => step.to > registration.as && step.to <= schema.version && !step.down)
|
|
32
|
+
.map((step) => `v${step.to}`);
|
|
33
|
+
return missing.length === 0
|
|
34
|
+
? { ok: true }
|
|
35
|
+
: { ok: false, reason: `v${registration.as} is unreachable — ${missing.join(', ')} declare no down migration` };
|
|
36
|
+
}
|
|
37
|
+
function project(intent, payload, as) {
|
|
38
|
+
const schema = options.schemaFor(intent);
|
|
39
|
+
if (!schema || as === undefined || as === schema.version)
|
|
40
|
+
return payload;
|
|
41
|
+
return schema.write(payload, { as }).payload;
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
addIntentListener(intent, handlerOptions) {
|
|
45
|
+
let forIntent = registrations.get(intent);
|
|
46
|
+
if (!forIntent) {
|
|
47
|
+
forIntent = new Set();
|
|
48
|
+
registrations.set(intent, forIntent);
|
|
49
|
+
}
|
|
50
|
+
const registration = {
|
|
51
|
+
intent,
|
|
52
|
+
consumer: options.consumer,
|
|
53
|
+
label: handlerOptions.label ?? options.consumer,
|
|
54
|
+
priority: handlerOptions.priority ?? 0,
|
|
55
|
+
...(handlerOptions.as === undefined ? {} : { as: handlerOptions.as }),
|
|
56
|
+
run: handlerOptions.handler,
|
|
57
|
+
};
|
|
58
|
+
forIntent.add(registration);
|
|
59
|
+
const unsubscribe = () => void forIntent.delete(registration);
|
|
60
|
+
handlerOptions.signal?.addEventListener('abort', unsubscribe, { once: true });
|
|
61
|
+
return unsubscribe;
|
|
62
|
+
},
|
|
63
|
+
candidates(intent) {
|
|
64
|
+
const eligible = [];
|
|
65
|
+
const ineligible = [];
|
|
66
|
+
for (const registration of registrations.get(intent) ?? []) {
|
|
67
|
+
(eligibility(registration).ok ? eligible : ineligible).push(strip(registration));
|
|
68
|
+
}
|
|
69
|
+
// Ranked here so every policy — and every chooser the host renders — sees one order.
|
|
70
|
+
eligible.sort((a, b) => b.priority - a.priority);
|
|
71
|
+
return { eligible, ineligible };
|
|
72
|
+
},
|
|
73
|
+
async raiseIntent(intent, payload, raiseOptions) {
|
|
74
|
+
const policy = raiseOptions?.resolve ?? 'first';
|
|
75
|
+
const all = [...(registrations.get(intent) ?? [])];
|
|
76
|
+
const eligible = [];
|
|
77
|
+
const ineligible = [];
|
|
78
|
+
for (const registration of all) {
|
|
79
|
+
const verdict = eligibility(registration);
|
|
80
|
+
if (verdict.ok)
|
|
81
|
+
eligible.push(registration);
|
|
82
|
+
else
|
|
83
|
+
ineligible.push({ candidate: strip(registration), reason: verdict.reason });
|
|
84
|
+
}
|
|
85
|
+
eligible.sort((a, b) => b.priority - a.priority);
|
|
86
|
+
if (eligible.length === 0)
|
|
87
|
+
throw new NoIntentHandlerError(intent, ineligible);
|
|
88
|
+
const run = async (registration) => ({
|
|
89
|
+
candidate: strip(registration),
|
|
90
|
+
result: (await registration.run(project(intent, payload, registration.as))),
|
|
91
|
+
});
|
|
92
|
+
if (policy === 'all') {
|
|
93
|
+
// Sequential, so handlers that touch the same records do so in a defined order — the same
|
|
94
|
+
// reason the outbox drains one at a time.
|
|
95
|
+
const handled = [];
|
|
96
|
+
for (const registration of eligible)
|
|
97
|
+
handled.push(await run(registration));
|
|
98
|
+
return { handled, ineligible };
|
|
99
|
+
}
|
|
100
|
+
let chosen;
|
|
101
|
+
if (policy === 'first') {
|
|
102
|
+
chosen = eligible[0];
|
|
103
|
+
}
|
|
104
|
+
else if (policy === 'ask') {
|
|
105
|
+
if (!raiseOptions?.chooser) {
|
|
106
|
+
throw new TypeError("[skew/data] resolve: 'ask' needs a `chooser` — which handler a user picks is a question " +
|
|
107
|
+
'for the shell that can render it, not for this package.');
|
|
108
|
+
}
|
|
109
|
+
const picked = await raiseOptions.chooser(eligible.map(strip), payload);
|
|
110
|
+
chosen = eligible.find((candidate) => matches(candidate, picked));
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
const picked = await policy(eligible.map(strip), payload);
|
|
114
|
+
chosen = eligible.find((candidate) => matches(candidate, picked));
|
|
115
|
+
}
|
|
116
|
+
if (!chosen) {
|
|
117
|
+
// A resolver that returns something not in the list is a bug in the resolver, and guessing
|
|
118
|
+
// would run a handler nobody selected.
|
|
119
|
+
throw new Error(`[skew/data] the resolver for "${intent}" returned a candidate that is not eligible`);
|
|
120
|
+
}
|
|
121
|
+
return { handled: [await run(chosen)], ineligible };
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const strip = (registration) => ({
|
|
126
|
+
intent: registration.intent,
|
|
127
|
+
consumer: registration.consumer,
|
|
128
|
+
label: registration.label,
|
|
129
|
+
priority: registration.priority,
|
|
130
|
+
...(registration.as === undefined ? {} : { as: registration.as }),
|
|
131
|
+
});
|
|
132
|
+
const matches = (registration, candidate) => registration.consumer === candidate.consumer &&
|
|
133
|
+
registration.label === candidate.label &&
|
|
134
|
+
registration.priority === candidate.priority &&
|
|
135
|
+
registration.as === candidate.as;
|
|
136
|
+
//# sourceMappingURL=intents.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"intents.js","sourceRoot":"","sources":["../../../../../libs/data/src/lib/intents.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tag invalidation, across every app on the page and — when asked — every tab.
|
|
3
|
+
*
|
|
4
|
+
* Tags rather than TTLs, because a time-to-live is a guess about when data went stale whereas a
|
|
5
|
+
* mutation *knows*. `character#1` and `characters` are claims about what a query depends on;
|
|
6
|
+
* invalidating a tag re-runs exactly the queries that said they cared.
|
|
7
|
+
*
|
|
8
|
+
* The reach is the part that is new. A per-application registry gives each app its own private
|
|
9
|
+
* answer, so a mutation in one leaves the other four stale — and staleness is invisible, which is
|
|
10
|
+
* the worst property a bug can have.
|
|
11
|
+
*/
|
|
12
|
+
export interface InvalidationOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Partition the signals belong to. Tags never cross one, so invalidating `characters` cannot
|
|
15
|
+
* reach another tenant's queries.
|
|
16
|
+
*/
|
|
17
|
+
partition: string;
|
|
18
|
+
/**
|
|
19
|
+
* Whether invalidation reaches other **JavaScript contexts** — other tabs, and other realms on
|
|
20
|
+
* this page.
|
|
21
|
+
*
|
|
22
|
+
* Named for contexts rather than tabs because the two cases are the same mechanism and people
|
|
23
|
+
* only think of the first one. A Braid realm is a separate context: two fragments composed onto
|
|
24
|
+
* one page are as isolated from each other, in memory, as two tabs are. Leave this off on a
|
|
25
|
+
* composed page and one fragment's invalidation silently never reaches another.
|
|
26
|
+
*
|
|
27
|
+
* Off by default, and that is a real choice rather than caution: refreshing a list in another tab
|
|
28
|
+
* is sometimes exactly right and sometimes a refetch storm across five of them.
|
|
29
|
+
*/
|
|
30
|
+
crossContext?: boolean;
|
|
31
|
+
/** Injectable for tests. */
|
|
32
|
+
channel?: BroadcastChannelLike;
|
|
33
|
+
}
|
|
34
|
+
/** The slice of `BroadcastChannel` this uses, so a test can supply a pair without a DOM. */
|
|
35
|
+
export interface BroadcastChannelLike {
|
|
36
|
+
postMessage(message: unknown): void;
|
|
37
|
+
addEventListener(type: 'message', listener: (event: {
|
|
38
|
+
data: unknown;
|
|
39
|
+
}) => void): void;
|
|
40
|
+
close(): void;
|
|
41
|
+
}
|
|
42
|
+
export interface Invalidator {
|
|
43
|
+
/** Marks tags stale. Every subscriber that owns a matching tag is notified. */
|
|
44
|
+
invalidate(...tags: string[]): void;
|
|
45
|
+
/**
|
|
46
|
+
* Registers interest. `getTags` is re-read on every invalidation, so a query whose tags depend on
|
|
47
|
+
* a changing input stays correct without re-subscribing.
|
|
48
|
+
*/
|
|
49
|
+
subscribe(getTags: () => readonly string[], notify: () => void, options?: {
|
|
50
|
+
signal?: AbortSignal;
|
|
51
|
+
}): () => void;
|
|
52
|
+
close(): void;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The invalidator for a partition, shared with anyone else who asks for the same one.
|
|
56
|
+
*
|
|
57
|
+
* Reference-counted: the underlying channel closes when the last holder closes, so a short-lived
|
|
58
|
+
* client cannot take the page's invalidation down with it.
|
|
59
|
+
*/
|
|
60
|
+
export declare function sharedInvalidator(options: InvalidationOptions): Invalidator;
|
|
61
|
+
/** Exposed for tests. */
|
|
62
|
+
export declare function resetSharedInvalidators(): void;
|
|
63
|
+
export declare function createInvalidator(options: InvalidationOptions): Invalidator;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tag invalidation, across every app on the page and — when asked — every tab.
|
|
3
|
+
*
|
|
4
|
+
* Tags rather than TTLs, because a time-to-live is a guess about when data went stale whereas a
|
|
5
|
+
* mutation *knows*. `character#1` and `characters` are claims about what a query depends on;
|
|
6
|
+
* invalidating a tag re-runs exactly the queries that said they cared.
|
|
7
|
+
*
|
|
8
|
+
* The reach is the part that is new. A per-application registry gives each app its own private
|
|
9
|
+
* answer, so a mutation in one leaves the other four stale — and staleness is invisible, which is
|
|
10
|
+
* the worst property a bug can have.
|
|
11
|
+
*/
|
|
12
|
+
const CHANNEL_NAME = 'skew:data:invalidation';
|
|
13
|
+
/**
|
|
14
|
+
* Invalidators, shared per partition within a JavaScript context.
|
|
15
|
+
*
|
|
16
|
+
* Two applications on one page each building a private invalidator is the same defect the outbox
|
|
17
|
+
* had: a page-wide fact with a per-app answer. A mutation in one would leave the other stale, and
|
|
18
|
+
* staleness is invisible — the worst property a bug can have.
|
|
19
|
+
*
|
|
20
|
+
* Cross-*realm* and cross-tab reach is `BroadcastChannel`'s job; this map covers the case the
|
|
21
|
+
* channel cannot, which is two clients in one context (a channel does not deliver to its own
|
|
22
|
+
* sender).
|
|
23
|
+
*/
|
|
24
|
+
const sharedInvalidators = new Map();
|
|
25
|
+
/**
|
|
26
|
+
* The invalidator for a partition, shared with anyone else who asks for the same one.
|
|
27
|
+
*
|
|
28
|
+
* Reference-counted: the underlying channel closes when the last holder closes, so a short-lived
|
|
29
|
+
* client cannot take the page's invalidation down with it.
|
|
30
|
+
*/
|
|
31
|
+
export function sharedInvalidator(options) {
|
|
32
|
+
const existing = sharedInvalidators.get(options.partition);
|
|
33
|
+
if (existing) {
|
|
34
|
+
existing.refs += 1;
|
|
35
|
+
return wrapRelease(options.partition, existing.invalidator);
|
|
36
|
+
}
|
|
37
|
+
const entry = { invalidator: createInvalidator(options), refs: 1 };
|
|
38
|
+
sharedInvalidators.set(options.partition, entry);
|
|
39
|
+
return wrapRelease(options.partition, entry.invalidator);
|
|
40
|
+
}
|
|
41
|
+
function wrapRelease(partition, invalidator) {
|
|
42
|
+
let released = false;
|
|
43
|
+
return {
|
|
44
|
+
invalidate: (...tags) => invalidator.invalidate(...tags),
|
|
45
|
+
subscribe: (getTags, notify, options) => invalidator.subscribe(getTags, notify, options),
|
|
46
|
+
close() {
|
|
47
|
+
if (released)
|
|
48
|
+
return;
|
|
49
|
+
released = true;
|
|
50
|
+
const entry = sharedInvalidators.get(partition);
|
|
51
|
+
if (!entry)
|
|
52
|
+
return;
|
|
53
|
+
entry.refs -= 1;
|
|
54
|
+
if (entry.refs > 0)
|
|
55
|
+
return;
|
|
56
|
+
sharedInvalidators.delete(partition);
|
|
57
|
+
entry.invalidator.close();
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** Exposed for tests. */
|
|
62
|
+
export function resetSharedInvalidators() {
|
|
63
|
+
for (const entry of sharedInvalidators.values())
|
|
64
|
+
entry.invalidator.close();
|
|
65
|
+
sharedInvalidators.clear();
|
|
66
|
+
}
|
|
67
|
+
export function createInvalidator(options) {
|
|
68
|
+
const subscribers = new Map();
|
|
69
|
+
const channel = options.channel ??
|
|
70
|
+
(options.crossContext && typeof BroadcastChannel !== 'undefined'
|
|
71
|
+
? new BroadcastChannel(CHANNEL_NAME)
|
|
72
|
+
: undefined);
|
|
73
|
+
// A BroadcastChannel does not deliver to the context that posted, which is the semantics wanted
|
|
74
|
+
// here: local subscribers are notified directly, remote ones over the channel, and neither path
|
|
75
|
+
// double-fires.
|
|
76
|
+
channel?.addEventListener('message', (event) => {
|
|
77
|
+
const signal = event.data;
|
|
78
|
+
if (signal?.partition !== options.partition)
|
|
79
|
+
return;
|
|
80
|
+
notifyLocal(signal.tags);
|
|
81
|
+
});
|
|
82
|
+
function notifyLocal(tags) {
|
|
83
|
+
if (tags.length === 0)
|
|
84
|
+
return;
|
|
85
|
+
for (const subscriber of [...subscribers.values()]) {
|
|
86
|
+
let interested = false;
|
|
87
|
+
try {
|
|
88
|
+
interested = subscriber.getTags().some((owned) => tags.some((tag) => tagsMatch(owned, tag)));
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// A tag getter that throws — a signal read outside its context, say — must not take down
|
|
92
|
+
// invalidation for everyone else.
|
|
93
|
+
interested = false;
|
|
94
|
+
}
|
|
95
|
+
if (!interested)
|
|
96
|
+
continue;
|
|
97
|
+
try {
|
|
98
|
+
subscriber.notify();
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// A refresh that throws is that query's problem, not this loop's.
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
invalidate(...tags) {
|
|
107
|
+
notifyLocal(tags);
|
|
108
|
+
channel?.postMessage({ partition: options.partition, tags });
|
|
109
|
+
},
|
|
110
|
+
subscribe(getTags, notify, subscribeOptions) {
|
|
111
|
+
const token = Symbol('skew-invalidation-subscriber');
|
|
112
|
+
subscribers.set(token, { getTags, notify });
|
|
113
|
+
const unsubscribe = () => void subscribers.delete(token);
|
|
114
|
+
subscribeOptions?.signal?.addEventListener('abort', unsubscribe, { once: true });
|
|
115
|
+
return unsubscribe;
|
|
116
|
+
},
|
|
117
|
+
close() {
|
|
118
|
+
subscribers.clear();
|
|
119
|
+
channel?.close();
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Wildcards in one direction: invalidating `character#*` reaches every `character#…` subscriber, and
|
|
125
|
+
* a subscriber may register the wildcard itself to hear about any change to that type.
|
|
126
|
+
*/
|
|
127
|
+
function tagsMatch(owned, invalidated) {
|
|
128
|
+
if (owned === invalidated)
|
|
129
|
+
return true;
|
|
130
|
+
if (invalidated.endsWith('*'))
|
|
131
|
+
return owned.startsWith(invalidated.slice(0, -1));
|
|
132
|
+
if (owned.endsWith('*'))
|
|
133
|
+
return invalidated.startsWith(owned.slice(0, -1));
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=invalidation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invalidation.js","sourceRoot":"","sources":["../../../../../libs/data/src/lib/invalidation.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-context mutual exclusion, for work that must happen once across every tab and realm.
|
|
3
|
+
*
|
|
4
|
+
* The motivating case is flushing the outbox. Storage is shared per origin, so without this every
|
|
5
|
+
* open tab drains the same queue on reconnect — replaying the same mutations several times, against
|
|
6
|
+
* a server that is by definition just coming back. No storage layer helps: IndexedDB makes each
|
|
7
|
+
* *write* atomic, and says nothing about who should be doing the writing.
|
|
8
|
+
*
|
|
9
|
+
* `navigator.locks` is the only primitive that spans realms and tabs. Where it is missing — Node,
|
|
10
|
+
* a non-secure context, an older browser — this degrades to an in-process lock, which is honest
|
|
11
|
+
* about what it can offer: exclusion within one JavaScript context, which is what existed before
|
|
12
|
+
* this and is still better than nothing.
|
|
13
|
+
*/
|
|
14
|
+
export type LockResult<T> = {
|
|
15
|
+
acquired: true;
|
|
16
|
+
value: T;
|
|
17
|
+
} | {
|
|
18
|
+
acquired: false;
|
|
19
|
+
};
|
|
20
|
+
export interface LockOptions {
|
|
21
|
+
/**
|
|
22
|
+
* When true (the default), a lock already held elsewhere is **not waited for** — the call returns
|
|
23
|
+
* `{ acquired: false }` immediately.
|
|
24
|
+
*
|
|
25
|
+
* Waiting is the wrong default for this. A flush that queued behind another tab's flush would run
|
|
26
|
+
* against a queue that tab had just drained, so the work is redundant by the time it starts;
|
|
27
|
+
* skipping says "someone is on it" and lets the caller move on.
|
|
28
|
+
*/
|
|
29
|
+
ifAvailable?: boolean;
|
|
30
|
+
signal?: AbortSignal;
|
|
31
|
+
}
|
|
32
|
+
/** True when this context has real cross-tab locks rather than the in-process fallback. */
|
|
33
|
+
export declare function hasCrossContextLocks(): boolean;
|
|
34
|
+
export declare function withLock<T>(name: string, run: () => Promise<T>, options?: LockOptions): Promise<LockResult<T>>;
|
|
35
|
+
/**
|
|
36
|
+
* The lock name for flushing one owner's queue.
|
|
37
|
+
*
|
|
38
|
+
* Per **owner**, not one global lock. Two different applications flush disjoint entries, so making
|
|
39
|
+
* them queue behind each other would serialize work that has no reason to be serialized. The
|
|
40
|
+
* conflict this prevents is the same application open in two tabs, both replaying the same
|
|
41
|
+
* mutations.
|
|
42
|
+
*/
|
|
43
|
+
export declare function outboxFlushLock(owner: string): string;
|