@bradensbay/globals-core 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -0
- package/dist/src/allocator.d.ts +78 -0
- package/dist/src/allocator.d.ts.map +1 -0
- package/dist/src/allocator.js +211 -0
- package/dist/src/allocator.js.map +1 -0
- package/dist/src/arena.d.ts +81 -0
- package/dist/src/arena.d.ts.map +1 -0
- package/dist/src/arena.js +224 -0
- package/dist/src/arena.js.map +1 -0
- package/dist/src/checksum.d.ts +5 -0
- package/dist/src/checksum.d.ts.map +1 -0
- package/dist/src/checksum.js +35 -0
- package/dist/src/checksum.js.map +1 -0
- package/dist/src/draft.d.ts +48 -0
- package/dist/src/draft.d.ts.map +1 -0
- package/dist/src/draft.js +427 -0
- package/dist/src/draft.js.map +1 -0
- package/dist/src/errors.d.ts +56 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +69 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/external.d.ts +52 -0
- package/dist/src/external.d.ts.map +1 -0
- package/dist/src/external.js +68 -0
- package/dist/src/external.js.map +1 -0
- package/dist/src/hamt.d.ts +87 -0
- package/dist/src/hamt.d.ts.map +1 -0
- package/dist/src/hamt.js +643 -0
- package/dist/src/hamt.js.map +1 -0
- package/dist/src/history.d.ts +45 -0
- package/dist/src/history.d.ts.map +1 -0
- package/dist/src/history.js +49 -0
- package/dist/src/history.js.map +1 -0
- package/dist/src/index.d.ts +55 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +38 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/inspect.d.ts +74 -0
- package/dist/src/inspect.d.ts.map +1 -0
- package/dist/src/inspect.js +127 -0
- package/dist/src/inspect.js.map +1 -0
- package/dist/src/layout.d.ts +130 -0
- package/dist/src/layout.d.ts.map +1 -0
- package/dist/src/layout.js +149 -0
- package/dist/src/layout.js.map +1 -0
- package/dist/src/liveness.d.ts +48 -0
- package/dist/src/liveness.d.ts.map +1 -0
- package/dist/src/liveness.js +92 -0
- package/dist/src/liveness.js.map +1 -0
- package/dist/src/owner.d.ts +130 -0
- package/dist/src/owner.d.ts.map +1 -0
- package/dist/src/owner.js +368 -0
- package/dist/src/owner.js.map +1 -0
- package/dist/src/reader.d.ts +122 -0
- package/dist/src/reader.d.ts.map +1 -0
- package/dist/src/reader.js +325 -0
- package/dist/src/reader.js.map +1 -0
- package/dist/src/readers.d.ts +60 -0
- package/dist/src/readers.d.ts.map +1 -0
- package/dist/src/readers.js +122 -0
- package/dist/src/readers.js.map +1 -0
- package/dist/src/retained.d.ts +39 -0
- package/dist/src/retained.d.ts.map +1 -0
- package/dist/src/retained.js +99 -0
- package/dist/src/retained.js.map +1 -0
- package/dist/src/schema.d.ts +82 -0
- package/dist/src/schema.d.ts.map +1 -0
- package/dist/src/schema.js +18 -0
- package/dist/src/schema.js.map +1 -0
- package/dist/src/store.d.ts +83 -0
- package/dist/src/store.d.ts.map +1 -0
- package/dist/src/store.js +127 -0
- package/dist/src/store.js.map +1 -0
- package/dist/src/strings.d.ts +37 -0
- package/dist/src/strings.d.ts.map +1 -0
- package/dist/src/strings.js +144 -0
- package/dist/src/strings.js.map +1 -0
- package/dist/src/tags.d.ts +47 -0
- package/dist/src/tags.d.ts.map +1 -0
- package/dist/src/tags.js +53 -0
- package/dist/src/tags.js.map +1 -0
- package/dist/src/values.d.ts +90 -0
- package/dist/src/values.d.ts.map +1 -0
- package/dist/src/values.js +458 -0
- package/dist/src/values.js.map +1 -0
- package/dist/src/vector.d.ts +64 -0
- package/dist/src/vector.d.ts.map +1 -0
- package/dist/src/vector.js +387 -0
- package/dist/src/vector.js.map +1 -0
- package/dist/src/verify.d.ts +38 -0
- package/dist/src/verify.d.ts.map +1 -0
- package/dist/src/verify.js +179 -0
- package/dist/src/verify.js.map +1 -0
- package/dist/src/view.d.ts +66 -0
- package/dist/src/view.d.ts.map +1 -0
- package/dist/src/view.js +278 -0
- package/dist/src/view.js.map +1 -0
- package/package.json +28 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { SharedArena } from "./arena.js";
|
|
2
|
+
import { ReaderTable } from "./readers.js";
|
|
3
|
+
import { RetainedRing } from "./retained.js";
|
|
4
|
+
import { Header } from "./layout.js";
|
|
5
|
+
import { StaleSnapshotError } from "./errors.js";
|
|
6
|
+
import { decodeValue } from "./values.js";
|
|
7
|
+
import { readPath, viewValue } from "./view.js";
|
|
8
|
+
import { verifyRoot } from "./verify.js";
|
|
9
|
+
/**
|
|
10
|
+
* How many times a reader retries the seqlock before it gives up on acquiring a fresh
|
|
11
|
+
* version. The window a commit holds the sequence odd is a handful of stores, so a reader
|
|
12
|
+
* that spins this many times is looking at a stalled or hostile writer, not contention.
|
|
13
|
+
*/
|
|
14
|
+
const MAX_SPIN = 128;
|
|
15
|
+
/**
|
|
16
|
+
* A pinned version.
|
|
17
|
+
*
|
|
18
|
+
* Holding one guarantees the graph beneath it cannot change, because the writer never
|
|
19
|
+
* mutates a published node. It does not guarantee the version stays retained forever: past
|
|
20
|
+
* the retention cap the owner force advances, and the next decode raises
|
|
21
|
+
* StaleSnapshotError rather than reading recycled memory.
|
|
22
|
+
*/
|
|
23
|
+
export class Snapshot {
|
|
24
|
+
versionId;
|
|
25
|
+
ownerGeneration;
|
|
26
|
+
#reader;
|
|
27
|
+
#slot;
|
|
28
|
+
#released = false;
|
|
29
|
+
#view;
|
|
30
|
+
constructor(reader, versionId, ownerGeneration, slot) {
|
|
31
|
+
this.#reader = reader;
|
|
32
|
+
this.versionId = versionId;
|
|
33
|
+
this.ownerGeneration = ownerGeneration;
|
|
34
|
+
this.#slot = slot;
|
|
35
|
+
}
|
|
36
|
+
get released() {
|
|
37
|
+
return this.#released;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The root value. Synchronous, no await, no round trip. This is the whole product.
|
|
41
|
+
*
|
|
42
|
+
* Validated on both sides of the decode. The check before it rejects a version that is
|
|
43
|
+
* already gone. The check after it is the one that matters: the owner raises the reclaim
|
|
44
|
+
* floor before it frees anything, so a version that is still above the floor once the
|
|
45
|
+
* decode has finished cannot have been freed while the decode was running. Validating
|
|
46
|
+
* only beforehand leaves a window in which a forced reclaim recycles the block and the
|
|
47
|
+
* reader returns a well formed but wrong value, which the soak harness found in about two
|
|
48
|
+
* reads out of four million.
|
|
49
|
+
*/
|
|
50
|
+
get value() {
|
|
51
|
+
this.validate();
|
|
52
|
+
const decoded = viewValue(this.#viewContext(), this.#slot);
|
|
53
|
+
this.validate();
|
|
54
|
+
return decoded;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The whole version as a detached plain value, with no proxies and no arena references.
|
|
58
|
+
*
|
|
59
|
+
* Use it to hand state to something that has to own it, such as a serialiser or a worker.
|
|
60
|
+
* It costs a full decode, which is exactly what the lazy path exists to avoid, so it is a
|
|
61
|
+
* deliberate call rather than the default.
|
|
62
|
+
*/
|
|
63
|
+
toJSON() {
|
|
64
|
+
this.validate();
|
|
65
|
+
const decoded = decodeValue(this.#reader.arena, this.#slot);
|
|
66
|
+
this.validate();
|
|
67
|
+
return decoded;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Read one path without building views for the nodes along it.
|
|
71
|
+
*
|
|
72
|
+
* The cheapest shape for a selector, and what the framework bindings use.
|
|
73
|
+
*/
|
|
74
|
+
get(path) {
|
|
75
|
+
this.validate();
|
|
76
|
+
const value = readPath(this.#reader.arena, this.#slot, path);
|
|
77
|
+
this.validate();
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The decode cache for this version.
|
|
82
|
+
*
|
|
83
|
+
* Built on first use and never shared with another version, so a reclaimed version cannot
|
|
84
|
+
* leave a decoded node reachable.
|
|
85
|
+
*/
|
|
86
|
+
#viewContext() {
|
|
87
|
+
if (this.#view === undefined) {
|
|
88
|
+
this.#view = {
|
|
89
|
+
arena: this.#reader.arena,
|
|
90
|
+
validate: () => this.validate(),
|
|
91
|
+
cache: new Map(),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return this.#view;
|
|
95
|
+
}
|
|
96
|
+
/** The root slot, for callers decoding lazily rather than eagerly. */
|
|
97
|
+
get root() {
|
|
98
|
+
this.validate();
|
|
99
|
+
return this.#slot;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Prove this version is still retained. Two atomic loads and two comparisons, run on both
|
|
103
|
+
* sides of every decode. Cheap enough to sit on the read path, and the only thing standing
|
|
104
|
+
* between a stalled reader and freed memory.
|
|
105
|
+
*/
|
|
106
|
+
validate() {
|
|
107
|
+
if (this.isValid())
|
|
108
|
+
return;
|
|
109
|
+
throw new StaleSnapshotError(this.versionId, this.#reader.reclaimFloor());
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* True when this snapshot can still be decoded. Never throws, and never constructs an
|
|
113
|
+
* error object, because it runs twice per read and a try/catch around the throwing form
|
|
114
|
+
* kept the whole path out of the optimising compiler.
|
|
115
|
+
*/
|
|
116
|
+
isValid() {
|
|
117
|
+
return (!this.#released &&
|
|
118
|
+
this.versionId >= this.#reader.reclaimFloor() &&
|
|
119
|
+
this.#reader.ownerGeneration() === this.ownerGeneration);
|
|
120
|
+
}
|
|
121
|
+
release() {
|
|
122
|
+
if (this.#released)
|
|
123
|
+
return;
|
|
124
|
+
this.#released = true;
|
|
125
|
+
this.#reader.releaseSnapshot(this);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* A reader.
|
|
130
|
+
*
|
|
131
|
+
* Attaching claims one slot in the reader table. Acquiring a snapshot is a seqlock read of
|
|
132
|
+
* the root, one atomic store to publish the pinned epoch, and one atomic load to confirm
|
|
133
|
+
* the version is above the reclaim floor. Nothing here blocks, allocates a promise, or
|
|
134
|
+
* sends a message.
|
|
135
|
+
*/
|
|
136
|
+
export class ArenaReader {
|
|
137
|
+
arena;
|
|
138
|
+
slot;
|
|
139
|
+
generation;
|
|
140
|
+
#table;
|
|
141
|
+
#ring;
|
|
142
|
+
#heartbeat;
|
|
143
|
+
/**
|
|
144
|
+
* The header view, held directly.
|
|
145
|
+
*
|
|
146
|
+
* Every hop on the read path costs: `this.arena.loadHeader(field)` is a property load, a
|
|
147
|
+
* method call, and a private field read before the atomic load itself. A read validates
|
|
148
|
+
* four times per value, so holding the array here rather than reaching through the arena
|
|
149
|
+
* on each one is worth the small duplication.
|
|
150
|
+
*/
|
|
151
|
+
#words;
|
|
152
|
+
#current;
|
|
153
|
+
#detached = false;
|
|
154
|
+
constructor(arena, options) {
|
|
155
|
+
this.arena = arena;
|
|
156
|
+
this.#words = arena.words;
|
|
157
|
+
this.#table = new ReaderTable(arena);
|
|
158
|
+
this.#ring = new RetainedRing(arena);
|
|
159
|
+
this.#heartbeat = options.heartbeat ?? true;
|
|
160
|
+
const claim = this.#table.claim();
|
|
161
|
+
this.slot = claim.slot;
|
|
162
|
+
this.generation = claim.generation;
|
|
163
|
+
}
|
|
164
|
+
static attach(buffer, options = {}) {
|
|
165
|
+
return new ArenaReader(SharedArena.attach(buffer), options);
|
|
166
|
+
}
|
|
167
|
+
get detached() {
|
|
168
|
+
return this.#detached;
|
|
169
|
+
}
|
|
170
|
+
reclaimFloor() {
|
|
171
|
+
return Atomics.load(this.#words, Header.ReclaimFloor);
|
|
172
|
+
}
|
|
173
|
+
ownerGeneration() {
|
|
174
|
+
return Atomics.load(this.#words, Header.OwnerGeneration);
|
|
175
|
+
}
|
|
176
|
+
/** The version the owner has currently published, without pinning it. */
|
|
177
|
+
publishedVersion() {
|
|
178
|
+
return Atomics.load(this.#words, Header.VersionId);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Acquire the current version and pin it.
|
|
182
|
+
*
|
|
183
|
+
* Returns the snapshot already held when the owner has not committed since, so a render
|
|
184
|
+
* loop that calls this every frame does not churn pins.
|
|
185
|
+
*/
|
|
186
|
+
acquire() {
|
|
187
|
+
if (this.#detached)
|
|
188
|
+
throw new Error("this reader has been detached");
|
|
189
|
+
const held = this.#current;
|
|
190
|
+
if (held !== undefined && held.versionId === this.publishedVersion() && held.isValid()) {
|
|
191
|
+
return held;
|
|
192
|
+
}
|
|
193
|
+
// Only a version change can require a longer view, so the growth check belongs here
|
|
194
|
+
// rather than on every header access.
|
|
195
|
+
this.arena.refresh();
|
|
196
|
+
this.#words = this.arena.words;
|
|
197
|
+
if (this.#heartbeat)
|
|
198
|
+
this.#table.beat(this.slot);
|
|
199
|
+
for (let attempt = 0; attempt < MAX_SPIN; attempt += 1) {
|
|
200
|
+
const words = this.#words;
|
|
201
|
+
const before = Atomics.load(words, Header.Sequence);
|
|
202
|
+
if ((before & 1) !== 0)
|
|
203
|
+
continue; // a commit is in progress
|
|
204
|
+
const tag = Atomics.load(words, Header.RootTag);
|
|
205
|
+
const payload = Atomics.load(words, Header.RootPayload);
|
|
206
|
+
const versionId = Atomics.load(words, Header.VersionId);
|
|
207
|
+
const ownerGeneration = Atomics.load(words, Header.OwnerGeneration);
|
|
208
|
+
// Inside the seqlock window, so the checksum belongs to the root just read rather than
|
|
209
|
+
// to whatever the writer published in the meantime.
|
|
210
|
+
const checksum = Atomics.load(words, Header.RootChecksum);
|
|
211
|
+
if (Atomics.load(words, Header.Sequence) !== before)
|
|
212
|
+
continue; // torn, retry
|
|
213
|
+
// Publish the pin before checking the floor. The owner publishes the floor before it
|
|
214
|
+
// frees anything and rescans afterwards, so one of the two checks always catches a
|
|
215
|
+
// race. See ArenaOwner.reclaim for the full argument.
|
|
216
|
+
this.#table.pin(this.slot, versionId);
|
|
217
|
+
if (versionId < this.reclaimFloor())
|
|
218
|
+
continue;
|
|
219
|
+
if (!this.#ring.isLive(versionId))
|
|
220
|
+
continue;
|
|
221
|
+
// Verified read mode, once per version rather than once per read. A render loop that
|
|
222
|
+
// reads the same version a hundred times pays for this once.
|
|
223
|
+
verifyRoot(this.arena, { tag, payload }, versionId, checksum);
|
|
224
|
+
const snapshot = new Snapshot(this, versionId, ownerGeneration, { tag, payload });
|
|
225
|
+
this.#current = snapshot;
|
|
226
|
+
return snapshot;
|
|
227
|
+
}
|
|
228
|
+
// The bounded spin is exhausted. Fall back to the last good snapshot when it is still
|
|
229
|
+
// valid, which keeps a render alive through a pathological writer rather than throwing
|
|
230
|
+
// into a render path that cannot handle it.
|
|
231
|
+
this.#table.unpin(this.slot);
|
|
232
|
+
if (held !== undefined && held.isValid()) {
|
|
233
|
+
this.#table.pin(this.slot, held.versionId);
|
|
234
|
+
return held;
|
|
235
|
+
}
|
|
236
|
+
throw new StaleSnapshotError(this.publishedVersion(), this.reclaimFloor());
|
|
237
|
+
}
|
|
238
|
+
/** Read the current root value. The synchronous read the whole library exists for. */
|
|
239
|
+
read() {
|
|
240
|
+
return this.acquire().value;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Pin a specific retained version rather than the current one.
|
|
244
|
+
*
|
|
245
|
+
* For debugging and time travel. It pins that version and everything after it against
|
|
246
|
+
* reclamation, so a snapshot obtained this way must be released. A render path should
|
|
247
|
+
* never use it.
|
|
248
|
+
*
|
|
249
|
+
* A reader owns one epoch slot, so this suspends the pin the render path was holding until
|
|
250
|
+
* the returned snapshot is released. A debug panel that browses history while a render
|
|
251
|
+
* reads should attach its own reader rather than share one, which costs a slot and keeps
|
|
252
|
+
* the two independent.
|
|
253
|
+
*/
|
|
254
|
+
acquireVersion(versionId) {
|
|
255
|
+
const entry = this.#ring.read(versionId);
|
|
256
|
+
if (entry === undefined || !this.#ring.isLive(versionId)) {
|
|
257
|
+
throw new StaleSnapshotError(versionId, this.reclaimFloor());
|
|
258
|
+
}
|
|
259
|
+
this.#table.pin(this.slot, versionId);
|
|
260
|
+
// Recheck after pinning, for the same reason acquire does: the pin has to be visible to
|
|
261
|
+
// the owner before the version can be trusted.
|
|
262
|
+
if (versionId < this.reclaimFloor() || !this.#ring.isLive(versionId)) {
|
|
263
|
+
this.#table.unpin(this.slot);
|
|
264
|
+
throw new StaleSnapshotError(versionId, this.reclaimFloor());
|
|
265
|
+
}
|
|
266
|
+
// A historical version is not verified: the published checksum covers the current root
|
|
267
|
+
// only, so there is nothing to compare a retained one against. Time travel is a debugging
|
|
268
|
+
// tool and this is a limit of it rather than an oversight.
|
|
269
|
+
return new Snapshot(this, versionId, this.ownerGeneration(), {
|
|
270
|
+
tag: entry.rootTag,
|
|
271
|
+
payload: entry.rootPayload,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
/** Materialise a retained version, pinning it only for the duration of the read. */
|
|
275
|
+
readVersion(versionId) {
|
|
276
|
+
const held = this.#current;
|
|
277
|
+
const snapshot = this.acquireVersion(versionId);
|
|
278
|
+
try {
|
|
279
|
+
return snapshot.toJSON();
|
|
280
|
+
}
|
|
281
|
+
finally {
|
|
282
|
+
// Restore the pin the caller had, so browsing history does not silently unpin the
|
|
283
|
+
// version a render is using.
|
|
284
|
+
if (held !== undefined && held.isValid())
|
|
285
|
+
this.#table.pin(this.slot, held.versionId);
|
|
286
|
+
else
|
|
287
|
+
this.#table.unpin(this.slot);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
releaseSnapshot(snapshot) {
|
|
291
|
+
if (this.#current === snapshot) {
|
|
292
|
+
this.#current = undefined;
|
|
293
|
+
this.#table.unpin(this.slot);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
// A historical snapshot from acquireVersion. A reader has one epoch slot, so pinning a
|
|
297
|
+
// past version suspends the pin the render path was holding. Releasing it restores that
|
|
298
|
+
// pin rather than leaving the reader unpinned or, worse, still holding the old version.
|
|
299
|
+
if (this.#table.pinnedEpoch(this.slot) !== snapshot.versionId)
|
|
300
|
+
return;
|
|
301
|
+
const held = this.#current;
|
|
302
|
+
if (held !== undefined && held.isValid())
|
|
303
|
+
this.#table.pin(this.slot, held.versionId);
|
|
304
|
+
else
|
|
305
|
+
this.#table.unpin(this.slot);
|
|
306
|
+
}
|
|
307
|
+
/** Release the pin and the reader slot. Always call this before dropping a reader. */
|
|
308
|
+
detach() {
|
|
309
|
+
if (this.#detached)
|
|
310
|
+
return;
|
|
311
|
+
this.#detached = true;
|
|
312
|
+
this.#current = undefined;
|
|
313
|
+
this.#table.release(this.slot);
|
|
314
|
+
}
|
|
315
|
+
stats() {
|
|
316
|
+
return {
|
|
317
|
+
slot: this.slot,
|
|
318
|
+
generation: this.generation,
|
|
319
|
+
pinnedEpoch: this.#table.pinnedEpoch(this.slot),
|
|
320
|
+
publishedVersion: this.publishedVersion(),
|
|
321
|
+
reclaimFloor: this.reclaimFloor(),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
//# sourceMappingURL=reader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reader.js","sourceRoot":"","sources":["../../src/reader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,WAAW,EAAa,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAoB,MAAM,WAAW,CAAC;AAClE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC;;;;GAIG;AACH,MAAM,QAAQ,GAAG,GAAG,CAAC;AAOrB;;;;;;;GAOG;AACH,MAAM,OAAO,QAAQ;IACV,SAAS,CAAS;IAClB,eAAe,CAAS;IACxB,OAAO,CAAc;IACrB,KAAK,CAAO;IACrB,SAAS,GAAG,KAAK,CAAC;IAClB,KAAK,CAA0B;IAE/B,YAAY,MAAmB,EAAE,SAAiB,EAAE,eAAuB,EAAE,IAAU;QACrF,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;QACtB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,eAAe,GAAG,eAAe,CAAC;QACvC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;IACpB,CAAC;IAED,IAAI,QAAQ;QACV,OAAO,IAAI,CAAC,SAAS,CAAC;IACxB,CAAC;IAED;;;;;;;;;;OAUG;IACH,IAAI,KAAK;QACP,IAAI,CAAC,QAAQ,EAAE,CAAC;QAChB,MAAM,OAAO,GAAG,SAAS,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3D,IAAI,CAAC,QAAQ,EAAE,CAAC;QAChB,OAAO,OAAO,CAAC;IACjB,CAAC;IAED;;;;;;OAMG;IACH,MAAM;QACJ,IAAI,CAAC,QAAQ,EAAE,CAAC;QAChB,MAAM,OAAO,GAAG,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;QAC5D,IAAI,CAAC,QAAQ,EAAE,CAAC;QAChB,OAAO,OAAO,CAAC;IACjB,CAAC;IAED;;;;OAIG;IACH,GAAG,CAAC,IAAkC;QACpC,IAAI,CAAC,QAAQ,EAAE,CAAC;QAChB,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QAC7D,IAAI,CAAC,QAAQ,EAAE,CAAC;QAChB,OAAO,KAAK,CAAC;IACf,CAAC;IAED;;;;;OAKG;IACH,YAAY;QACV,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YAC7B,IAAI,CAAC,KAAK,GAAG;gBACX,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,KAAK;gBACzB,QAAQ,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,QAAQ,EAAE;gBAC/B,KAAK,EAAE,IAAI,GAAG,EAAmB;aAClC,CAAC;QACJ,CAAC;QACD,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IAED,sEAAsE;IACtE,IAAI,IAAI;QACN,IAAI,CAAC,QAAQ,EAAE,CAAC;QAChB,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IAED;;;;OAIG;IACH,QAAQ;QACN,IAAI,IAAI,CAAC,OAAO,EAAE;YAAE,OAAO;QAC3B,MAAM,IAAI,kBAAkB,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;IAC5E,CAAC;IAED;;;;OAIG;IACH,OAAO;QACL,OAAO,CACL,CAAC,IAAI,CAAC,SAAS;YACf,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE;YAC7C,IAAI,CAAC,OAAO,CAAC,eAAe,EAAE,KAAK,IAAI,CAAC,eAAe,CACxD,CAAC;IACJ,CAAC;IAED,OAAO;QACL,IAAI,IAAI,CAAC,SAAS;YAAE,OAAO;QAC3B,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACtB,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;IACrC,CAAC;CACF;AAOD;;;;;;;GAOG;AACH,MAAM,OAAO,WAAW;IACb,KAAK,CAAc;IACnB,IAAI,CAAS;IACb,UAAU,CAAS;IACnB,MAAM,CAAc;IACpB,KAAK,CAAe;IACpB,UAAU,CAAU;IAC7B;;;;;;;OAOG;IACH,MAAM,CAAa;IACnB,QAAQ,CAAuB;IAC/B,SAAS,GAAG,KAAK,CAAC;IAElB,YAAoB,KAAkB,EAAE,OAAsB;QAC5D,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC;QAC1B,IAAI,CAAC,MAAM,GAAG,IAAI,WAAW,CAAC,KAAK,CAAC,CAAC;QACrC,IAAI,CAAC,KAAK,GAAG,IAAI,YAAY,CAAC,KAAK,CAAC,CAAC;QACrC,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,SAAS,IAAI,IAAI,CAAC;QAC5C,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QAClC,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;QACvB,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC,UAAU,CAAC;IACrC,CAAC;IAED,MAAM,CAAC,MAAM,CAAC,MAAyB,EAAE,OAAO,GAAkB,EAAE;QAClE,OAAO,IAAI,WAAW,CAAC,WAAW,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC;IAC9D,CAAC;IAED,IAAI,QAAQ;QACV,OAAO,IAAI,CAAC,SAAS,CAAC;IACxB,CAAC;IAED,YAAY;QACV,OAAO,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC;IACxD,CAAC;IAED,eAAe;QACb,OAAO,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,eAAe,CAAC,CAAC;IAC3D,CAAC;IAED,yEAAyE;IACzE,gBAAgB;QACd,OAAO,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC;IACrD,CAAC;IAED;;;;;OAKG;IACH,OAAO;QACL,IAAI,IAAI,CAAC,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;QAErE,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC3B,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,SAAS,KAAK,IAAI,CAAC,gBAAgB,EAAE,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC;YACvF,OAAO,IAAI,CAAC;QACd,CAAC;QAED,oFAAoF;QACpF,sCAAsC;QACtC,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;QACrB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC;QAE/B,IAAI,IAAI,CAAC,UAAU;YAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAEjD,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,QAAQ,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC;YACvD,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC;YAC1B,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;YACpD,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC;gBAAE,SAAS,CAAC,0BAA0B;YAE5D,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;YAChD,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC;YACxD,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC;YACxD,MAAM,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,eAAe,CAAC,CAAC;YACpE,uFAAuF;YACvF,oDAAoD;YACpD,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC;YAE1D,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC,KAAK,MAAM;gBAAE,SAAS,CAAC,cAAc;YAE7E,qFAAqF;YACrF,mFAAmF;YACnF,sDAAsD;YACtD,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;YAEtC,IAAI,SAAS,GAAG,IAAI,CAAC,YAAY,EAAE;gBAAE,SAAS;YAC9C,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC;gBAAE,SAAS;YAE5C,qFAAqF;YACrF,6DAA6D;YAC7D,UAAU,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC;YAE9D,MAAM,QAAQ,GAAG,IAAI,QAAQ,CAAC,IAAI,EAAE,SAAS,EAAE,eAAe,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC;YAClF,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;YACzB,OAAO,QAAQ,CAAC;QAClB,CAAC;QAED,sFAAsF;QACtF,uFAAuF;QACvF,4CAA4C;QAC5C,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7B,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC;YACzC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;YAC3C,OAAO,IAAI,CAAC;QACd,CAAC;QACD,MAAM,IAAI,kBAAkB,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC;IAC7E,CAAC;IAED,sFAAsF;IACtF,IAAI;QACF,OAAO,IAAI,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC;IAC9B,CAAC;IAED;;;;;;;;;;;OAWG;IACH,cAAc,CAAC,SAAiB;QAC9B,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACzC,IAAI,KAAK,KAAK,SAAS,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC;YACzD,MAAM,IAAI,kBAAkB,CAAC,SAAS,EAAE,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC;QAC/D,CAAC;QAED,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;QAEtC,wFAAwF;QACxF,+CAA+C;QAC/C,IAAI,SAAS,GAAG,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC;YACrE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC7B,MAAM,IAAI,kBAAkB,CAAC,SAAS,EAAE,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC;QAC/D,CAAC;QAED,uFAAuF;QACvF,0FAA0F;QAC1F,2DAA2D;QAC3D,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,eAAe,EAAE,EAAE;YAC3D,GAAG,EAAE,KAAK,CAAC,OAAO;YAClB,OAAO,EAAE,KAAK,CAAC,WAAW;SAC3B,CAAC,CAAC;IACL,CAAC;IAED,oFAAoF;IACpF,WAAW,CAAC,SAAiB;QAC3B,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC3B,MAAM,QAAQ,GAAG,IAAI,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC;QAChD,IAAI,CAAC;YACH,OAAO,QAAQ,CAAC,MAAM,EAAE,CAAC;QAC3B,CAAC;gBAAS,CAAC;YACT,kFAAkF;YAClF,6BAA6B;YAC7B,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,OAAO,EAAE;gBAAE,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;;gBAChF,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACpC,CAAC;IACH,CAAC;IAED,eAAe,CAAC,QAAkB;QAChC,IAAI,IAAI,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YAC/B,IAAI,CAAC,QAAQ,GAAG,SAAS,CAAC;YAC1B,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC7B,OAAO;QACT,CAAC;QAED,uFAAuF;QACvF,wFAAwF;QACxF,wFAAwF;QACxF,IAAI,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,QAAQ,CAAC,SAAS;YAAE,OAAO;QACtE,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC3B,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,OAAO,EAAE;YAAE,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;;YAChF,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACpC,CAAC;IAED,sFAAsF;IACtF,MAAM;QACJ,IAAI,IAAI,CAAC,SAAS;YAAE,OAAO;QAC3B,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACtB,IAAI,CAAC,QAAQ,GAAG,SAAS,CAAC;QAC1B,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjC,CAAC;IAED,KAAK;QAOH,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,UAAU,EAAE,IAAI,CAAC,UAAU;YAC3B,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC;YAC/C,gBAAgB,EAAE,IAAI,CAAC,gBAAgB,EAAE;YACzC,YAAY,EAAE,IAAI,CAAC,YAAY,EAAE;SAClC,CAAC;IACJ,CAAC;CACF"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { SharedArena } from "./arena.js";
|
|
2
|
+
/**
|
|
3
|
+
* The reader table.
|
|
4
|
+
*
|
|
5
|
+
* One slot per reader, claimed with a compare and exchange so two processes racing for the
|
|
6
|
+
* same slot cannot both win. A slot carries four words: state, generation, pinned epoch,
|
|
7
|
+
* and a heartbeat counter.
|
|
8
|
+
*
|
|
9
|
+
* The generation exists for the reload case. A renderer that reloads discards its heap
|
|
10
|
+
* while its slot is still claimed. When the owner declares that reader dead and frees the
|
|
11
|
+
* slot, the next claimant bumps the generation, so a snapshot taken by the previous
|
|
12
|
+
* occupant can be recognised as belonging to a different reader and fails closed.
|
|
13
|
+
*/
|
|
14
|
+
export declare class ReaderTable {
|
|
15
|
+
#private;
|
|
16
|
+
constructor(arena: SharedArena);
|
|
17
|
+
get capacity(): number;
|
|
18
|
+
/** Claim a free slot. Throws when every slot is taken rather than silently sharing one. */
|
|
19
|
+
claim(): {
|
|
20
|
+
slot: number;
|
|
21
|
+
generation: number;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Release a slot. Unpins first, so a writer that observes the release can immediately
|
|
25
|
+
* reclaim the version this reader was holding.
|
|
26
|
+
*/
|
|
27
|
+
release(slot: number): void;
|
|
28
|
+
/** Pin this reader to a version. One atomic store, which is the whole cost of entering. */
|
|
29
|
+
pin(slot: number, versionId: number): void;
|
|
30
|
+
unpin(slot: number): void;
|
|
31
|
+
pinnedEpoch(slot: number): number;
|
|
32
|
+
generation(slot: number): number;
|
|
33
|
+
isClaimed(slot: number): boolean;
|
|
34
|
+
beat(slot: number): void;
|
|
35
|
+
heartbeat(slot: number): number;
|
|
36
|
+
/**
|
|
37
|
+
* The oldest version any reader is pinned to, or `Infinity` when nothing is pinned.
|
|
38
|
+
* Everything strictly older can be reclaimed.
|
|
39
|
+
*
|
|
40
|
+
* This is a scan of the table on each commit. With a table of 32 slots that is 32 atomic
|
|
41
|
+
* loads, which is cheaper than maintaining a shared minimum that every reader would have
|
|
42
|
+
* to update on entry and exit.
|
|
43
|
+
*/
|
|
44
|
+
minimumPinnedEpoch(): number;
|
|
45
|
+
/** Every claimed slot, for liveness checks and diagnostics. */
|
|
46
|
+
claimedSlots(): Array<{
|
|
47
|
+
slot: number;
|
|
48
|
+
epoch: number;
|
|
49
|
+
generation: number;
|
|
50
|
+
heartbeat: number;
|
|
51
|
+
}>;
|
|
52
|
+
/**
|
|
53
|
+
* Force a slot free. Used by the owner once it has declared a reader dead, and only
|
|
54
|
+
* then. A live reader whose slot is taken this way will fail its next validity check and
|
|
55
|
+
* fall back to reacquiring, which is the fail closed path rather than a crash.
|
|
56
|
+
*/
|
|
57
|
+
forceRelease(slot: number): void;
|
|
58
|
+
ownerHeartbeat(): number;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=readers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"readers.d.ts","sourceRoot":"","sources":["../../src/readers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAW9C;;;;;;;;;;;GAWG;AACH,qBAAa,WAAW;;IAGtB,YAAY,KAAK,EAAE,WAAW,EAE7B;IAED,IAAI,QAAQ,IAAI,MAAM,CAErB;IAQD,2FAA2F;IAC3F,KAAK,IAAI;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAc5C;IAED;;;OAGG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAI1B;IAED,2FAA2F;IAC3F,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAEzC;IAED,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAExB;IAED,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhC;IAED,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/B;IAED,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAI/B;IAED,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAEvB;IAED,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9B;IAED;;;;;;;OAOG;IACH,kBAAkB,IAAI,MAAM,CAS3B;IAED,+DAA+D;IAC/D,YAAY,IAAI,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAY5F;IAED;;;;OAIG;IACH,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAK/B;IAED,cAAc,IAAI,MAAM,CAEvB;CACF"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { NoReaderSlotError } from "./errors.js";
|
|
2
|
+
import { Header, READER_SLOT_WORDS, ReaderSlot, SLOT_CLAIMED, SLOT_FREE, WORD, } from "./layout.js";
|
|
3
|
+
/**
|
|
4
|
+
* The reader table.
|
|
5
|
+
*
|
|
6
|
+
* One slot per reader, claimed with a compare and exchange so two processes racing for the
|
|
7
|
+
* same slot cannot both win. A slot carries four words: state, generation, pinned epoch,
|
|
8
|
+
* and a heartbeat counter.
|
|
9
|
+
*
|
|
10
|
+
* The generation exists for the reload case. A renderer that reloads discards its heap
|
|
11
|
+
* while its slot is still claimed. When the owner declares that reader dead and frees the
|
|
12
|
+
* slot, the next claimant bumps the generation, so a snapshot taken by the previous
|
|
13
|
+
* occupant can be recognised as belonging to a different reader and fails closed.
|
|
14
|
+
*/
|
|
15
|
+
export class ReaderTable {
|
|
16
|
+
#arena;
|
|
17
|
+
constructor(arena) {
|
|
18
|
+
this.#arena = arena;
|
|
19
|
+
}
|
|
20
|
+
get capacity() {
|
|
21
|
+
return this.#arena.geometry.maxReaders;
|
|
22
|
+
}
|
|
23
|
+
#wordIndex(slot, field) {
|
|
24
|
+
return (this.#arena.geometry.readerTableOffset / WORD + slot * READER_SLOT_WORDS + field);
|
|
25
|
+
}
|
|
26
|
+
/** Claim a free slot. Throws when every slot is taken rather than silently sharing one. */
|
|
27
|
+
claim() {
|
|
28
|
+
const words = this.#arena.words;
|
|
29
|
+
for (let slot = 0; slot < this.capacity; slot += 1) {
|
|
30
|
+
const stateIndex = this.#wordIndex(slot, ReaderSlot.State);
|
|
31
|
+
if (Atomics.compareExchange(words, stateIndex, SLOT_FREE, SLOT_CLAIMED) !== SLOT_FREE) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const generation = Atomics.add(words, this.#wordIndex(slot, ReaderSlot.Generation), 1) + 1;
|
|
35
|
+
Atomics.store(words, this.#wordIndex(slot, ReaderSlot.Epoch), 0);
|
|
36
|
+
Atomics.store(words, this.#wordIndex(slot, ReaderSlot.Heartbeat), 0);
|
|
37
|
+
return { slot, generation };
|
|
38
|
+
}
|
|
39
|
+
throw new NoReaderSlotError(this.capacity);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Release a slot. Unpins first, so a writer that observes the release can immediately
|
|
43
|
+
* reclaim the version this reader was holding.
|
|
44
|
+
*/
|
|
45
|
+
release(slot) {
|
|
46
|
+
const words = this.#arena.words;
|
|
47
|
+
Atomics.store(words, this.#wordIndex(slot, ReaderSlot.Epoch), 0);
|
|
48
|
+
Atomics.store(words, this.#wordIndex(slot, ReaderSlot.State), SLOT_FREE);
|
|
49
|
+
}
|
|
50
|
+
/** Pin this reader to a version. One atomic store, which is the whole cost of entering. */
|
|
51
|
+
pin(slot, versionId) {
|
|
52
|
+
Atomics.store(this.#arena.words, this.#wordIndex(slot, ReaderSlot.Epoch), versionId);
|
|
53
|
+
}
|
|
54
|
+
unpin(slot) {
|
|
55
|
+
Atomics.store(this.#arena.words, this.#wordIndex(slot, ReaderSlot.Epoch), 0);
|
|
56
|
+
}
|
|
57
|
+
pinnedEpoch(slot) {
|
|
58
|
+
return Atomics.load(this.#arena.words, this.#wordIndex(slot, ReaderSlot.Epoch));
|
|
59
|
+
}
|
|
60
|
+
generation(slot) {
|
|
61
|
+
return Atomics.load(this.#arena.words, this.#wordIndex(slot, ReaderSlot.Generation));
|
|
62
|
+
}
|
|
63
|
+
isClaimed(slot) {
|
|
64
|
+
return (Atomics.load(this.#arena.words, this.#wordIndex(slot, ReaderSlot.State)) === SLOT_CLAIMED);
|
|
65
|
+
}
|
|
66
|
+
beat(slot) {
|
|
67
|
+
Atomics.add(this.#arena.words, this.#wordIndex(slot, ReaderSlot.Heartbeat), 1);
|
|
68
|
+
}
|
|
69
|
+
heartbeat(slot) {
|
|
70
|
+
return Atomics.load(this.#arena.words, this.#wordIndex(slot, ReaderSlot.Heartbeat));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The oldest version any reader is pinned to, or `Infinity` when nothing is pinned.
|
|
74
|
+
* Everything strictly older can be reclaimed.
|
|
75
|
+
*
|
|
76
|
+
* This is a scan of the table on each commit. With a table of 32 slots that is 32 atomic
|
|
77
|
+
* loads, which is cheaper than maintaining a shared minimum that every reader would have
|
|
78
|
+
* to update on entry and exit.
|
|
79
|
+
*/
|
|
80
|
+
minimumPinnedEpoch() {
|
|
81
|
+
const words = this.#arena.words;
|
|
82
|
+
let minimum = Number.POSITIVE_INFINITY;
|
|
83
|
+
for (let slot = 0; slot < this.capacity; slot += 1) {
|
|
84
|
+
if (Atomics.load(words, this.#wordIndex(slot, ReaderSlot.State)) !== SLOT_CLAIMED)
|
|
85
|
+
continue;
|
|
86
|
+
const epoch = Atomics.load(words, this.#wordIndex(slot, ReaderSlot.Epoch));
|
|
87
|
+
if (epoch !== 0 && epoch < minimum)
|
|
88
|
+
minimum = epoch;
|
|
89
|
+
}
|
|
90
|
+
return minimum;
|
|
91
|
+
}
|
|
92
|
+
/** Every claimed slot, for liveness checks and diagnostics. */
|
|
93
|
+
claimedSlots() {
|
|
94
|
+
const out = [];
|
|
95
|
+
for (let slot = 0; slot < this.capacity; slot += 1) {
|
|
96
|
+
if (!this.isClaimed(slot))
|
|
97
|
+
continue;
|
|
98
|
+
out.push({
|
|
99
|
+
slot,
|
|
100
|
+
epoch: this.pinnedEpoch(slot),
|
|
101
|
+
generation: this.generation(slot),
|
|
102
|
+
heartbeat: this.heartbeat(slot),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Force a slot free. Used by the owner once it has declared a reader dead, and only
|
|
109
|
+
* then. A live reader whose slot is taken this way will fail its next validity check and
|
|
110
|
+
* fall back to reacquiring, which is the fail closed path rather than a crash.
|
|
111
|
+
*/
|
|
112
|
+
forceRelease(slot) {
|
|
113
|
+
const words = this.#arena.words;
|
|
114
|
+
Atomics.store(words, this.#wordIndex(slot, ReaderSlot.Epoch), 0);
|
|
115
|
+
Atomics.add(words, this.#wordIndex(slot, ReaderSlot.Generation), 1);
|
|
116
|
+
Atomics.store(words, this.#wordIndex(slot, ReaderSlot.State), SLOT_FREE);
|
|
117
|
+
}
|
|
118
|
+
ownerHeartbeat() {
|
|
119
|
+
return this.#arena.loadHeader(Header.OwnerHeartbeat);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=readers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"readers.js","sourceRoot":"","sources":["../../src/readers.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { SharedArena } from "./arena.js";
|
|
2
|
+
export interface RetainedVersion {
|
|
3
|
+
readonly versionId: number;
|
|
4
|
+
readonly rootTag: number;
|
|
5
|
+
readonly rootPayload: number;
|
|
6
|
+
readonly state: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The retained version ring.
|
|
10
|
+
*
|
|
11
|
+
* The owner appends one entry per commit. Readers only read it, and they read it for one
|
|
12
|
+
* reason: to prove that the version they are pinned to is still live before they decode
|
|
13
|
+
* anything through it.
|
|
14
|
+
*
|
|
15
|
+
* The ring is bounded on purpose. A reader that stops advancing cannot pin memory forever,
|
|
16
|
+
* because once the ring wraps the owner force reclaims the oldest entry and that reader
|
|
17
|
+
* fails closed on its next decode. Unbounded retention would be the alternative, and it
|
|
18
|
+
* turns one frozen window into an out of memory crash for the whole application.
|
|
19
|
+
*/
|
|
20
|
+
export declare class RetainedRing {
|
|
21
|
+
#private;
|
|
22
|
+
constructor(arena: SharedArena);
|
|
23
|
+
get capacity(): number;
|
|
24
|
+
/** Owner only. Publish a version into the ring, overwriting the entry it wraps onto. */
|
|
25
|
+
publish(versionId: number, rootTag: number, rootPayload: number): void;
|
|
26
|
+
/** Owner only. Mark a version reclaimed. `forced` records that a reader was still pinned. */
|
|
27
|
+
retire(versionId: number, forced: boolean): void;
|
|
28
|
+
/**
|
|
29
|
+
* The check every reader runs after publishing its epoch. Returns true only when the
|
|
30
|
+
* version is still live in the ring, which proves the owner has not reclaimed the memory
|
|
31
|
+
* beneath it.
|
|
32
|
+
*/
|
|
33
|
+
isLive(versionId: number): boolean;
|
|
34
|
+
read(versionId: number): RetainedVersion | undefined;
|
|
35
|
+
/** Live versions, oldest first. Diagnostics and the debug panel use this. */
|
|
36
|
+
live(): RetainedVersion[];
|
|
37
|
+
reclaimFloor(): number;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=retained.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retained.d.ts","sourceRoot":"","sources":["../../src/retained.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAS9C,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,YAAY;;IAGvB,YAAY,KAAK,EAAE,WAAW,EAE7B;IAED,IAAI,QAAQ,IAAI,MAAM,CAErB;IAeD,wFAAwF;IACxF,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAUrE;IAED,6FAA6F;IAC7F,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI,CAS/C;IAED;;;;OAIG;IACH,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAQjC;IAED,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAYnD;IAED,6EAA6E;IAC7E,IAAI,IAAI,eAAe,EAAE,CAgBxB;IAED,YAAY,IAAI,MAAM,CAErB;CACF"}
|