@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.
Files changed (98) hide show
  1. package/README.md +69 -0
  2. package/dist/src/allocator.d.ts +78 -0
  3. package/dist/src/allocator.d.ts.map +1 -0
  4. package/dist/src/allocator.js +211 -0
  5. package/dist/src/allocator.js.map +1 -0
  6. package/dist/src/arena.d.ts +81 -0
  7. package/dist/src/arena.d.ts.map +1 -0
  8. package/dist/src/arena.js +224 -0
  9. package/dist/src/arena.js.map +1 -0
  10. package/dist/src/checksum.d.ts +5 -0
  11. package/dist/src/checksum.d.ts.map +1 -0
  12. package/dist/src/checksum.js +35 -0
  13. package/dist/src/checksum.js.map +1 -0
  14. package/dist/src/draft.d.ts +48 -0
  15. package/dist/src/draft.d.ts.map +1 -0
  16. package/dist/src/draft.js +427 -0
  17. package/dist/src/draft.js.map +1 -0
  18. package/dist/src/errors.d.ts +56 -0
  19. package/dist/src/errors.d.ts.map +1 -0
  20. package/dist/src/errors.js +69 -0
  21. package/dist/src/errors.js.map +1 -0
  22. package/dist/src/external.d.ts +52 -0
  23. package/dist/src/external.d.ts.map +1 -0
  24. package/dist/src/external.js +68 -0
  25. package/dist/src/external.js.map +1 -0
  26. package/dist/src/hamt.d.ts +87 -0
  27. package/dist/src/hamt.d.ts.map +1 -0
  28. package/dist/src/hamt.js +643 -0
  29. package/dist/src/hamt.js.map +1 -0
  30. package/dist/src/history.d.ts +45 -0
  31. package/dist/src/history.d.ts.map +1 -0
  32. package/dist/src/history.js +49 -0
  33. package/dist/src/history.js.map +1 -0
  34. package/dist/src/index.d.ts +55 -0
  35. package/dist/src/index.d.ts.map +1 -0
  36. package/dist/src/index.js +38 -0
  37. package/dist/src/index.js.map +1 -0
  38. package/dist/src/inspect.d.ts +74 -0
  39. package/dist/src/inspect.d.ts.map +1 -0
  40. package/dist/src/inspect.js +127 -0
  41. package/dist/src/inspect.js.map +1 -0
  42. package/dist/src/layout.d.ts +130 -0
  43. package/dist/src/layout.d.ts.map +1 -0
  44. package/dist/src/layout.js +149 -0
  45. package/dist/src/layout.js.map +1 -0
  46. package/dist/src/liveness.d.ts +48 -0
  47. package/dist/src/liveness.d.ts.map +1 -0
  48. package/dist/src/liveness.js +92 -0
  49. package/dist/src/liveness.js.map +1 -0
  50. package/dist/src/owner.d.ts +130 -0
  51. package/dist/src/owner.d.ts.map +1 -0
  52. package/dist/src/owner.js +368 -0
  53. package/dist/src/owner.js.map +1 -0
  54. package/dist/src/reader.d.ts +122 -0
  55. package/dist/src/reader.d.ts.map +1 -0
  56. package/dist/src/reader.js +325 -0
  57. package/dist/src/reader.js.map +1 -0
  58. package/dist/src/readers.d.ts +60 -0
  59. package/dist/src/readers.d.ts.map +1 -0
  60. package/dist/src/readers.js +122 -0
  61. package/dist/src/readers.js.map +1 -0
  62. package/dist/src/retained.d.ts +39 -0
  63. package/dist/src/retained.d.ts.map +1 -0
  64. package/dist/src/retained.js +99 -0
  65. package/dist/src/retained.js.map +1 -0
  66. package/dist/src/schema.d.ts +82 -0
  67. package/dist/src/schema.d.ts.map +1 -0
  68. package/dist/src/schema.js +18 -0
  69. package/dist/src/schema.js.map +1 -0
  70. package/dist/src/store.d.ts +83 -0
  71. package/dist/src/store.d.ts.map +1 -0
  72. package/dist/src/store.js +127 -0
  73. package/dist/src/store.js.map +1 -0
  74. package/dist/src/strings.d.ts +37 -0
  75. package/dist/src/strings.d.ts.map +1 -0
  76. package/dist/src/strings.js +144 -0
  77. package/dist/src/strings.js.map +1 -0
  78. package/dist/src/tags.d.ts +47 -0
  79. package/dist/src/tags.d.ts.map +1 -0
  80. package/dist/src/tags.js +53 -0
  81. package/dist/src/tags.js.map +1 -0
  82. package/dist/src/values.d.ts +90 -0
  83. package/dist/src/values.d.ts.map +1 -0
  84. package/dist/src/values.js +458 -0
  85. package/dist/src/values.js.map +1 -0
  86. package/dist/src/vector.d.ts +64 -0
  87. package/dist/src/vector.d.ts.map +1 -0
  88. package/dist/src/vector.js +387 -0
  89. package/dist/src/vector.js.map +1 -0
  90. package/dist/src/verify.d.ts +38 -0
  91. package/dist/src/verify.d.ts.map +1 -0
  92. package/dist/src/verify.js +179 -0
  93. package/dist/src/verify.js.map +1 -0
  94. package/dist/src/view.d.ts +66 -0
  95. package/dist/src/view.d.ts.map +1 -0
  96. package/dist/src/view.js +278 -0
  97. package/dist/src/view.js.map +1 -0
  98. package/package.json +28 -0
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # @bradensbay/globals-core
2
+
3
+ The runtime agnostic core of [Globals](https://github.com/christianGRogers/globals): one
4
+ shared memory arena, a tagged value encoding, an allocator, and epoch based reclamation.
5
+
6
+ Nothing in this package imports Electron. That is deliberate, and it is what makes the arena
7
+ testable in plain Node with worker threads.
8
+
9
+ ## Contract
10
+
11
+ Reads are synchronous. Writes are asynchronous. See
12
+ [docs/contract.md](../../docs/contract.md).
13
+
14
+ ## Usage
15
+
16
+ ```ts
17
+ import { ArenaOwner, ArenaReader } from "@bradensbay/globals-core";
18
+
19
+ // In the owner, the only process that writes.
20
+ const owner = ArenaOwner.create({ byteLength: 1 << 20 });
21
+ owner.commit(42);
22
+
23
+ // In any process that holds the same buffer.
24
+ const reader = ArenaReader.attach(owner.buffer);
25
+ reader.read(); // 42, synchronously
26
+
27
+ // Hold a version across several reads so they cannot disagree.
28
+ const snapshot = reader.acquire();
29
+ snapshot.value;
30
+ snapshot.release();
31
+
32
+ reader.detach();
33
+ ```
34
+
35
+ ## Writing
36
+
37
+ ```ts
38
+ await store.update((draft: State) => {
39
+ draft.users[3].name = "new name";
40
+ });
41
+ ```
42
+
43
+ Only the paths the recipe touched are rebuilt. Setting one key of an object with ten
44
+ thousand keys allocates fewer than twenty blocks rather than copying the record. See
45
+ [docs/object-layer.md](../../docs/object-layer.md).
46
+
47
+ ## Type ladder
48
+
49
+ Plain objects, arrays, `Map`, `Set`, `Date`, `RegExp`, `BigInt`, typed arrays, and the
50
+ scalars. Anything else raises `UnencodableValueError` rather than being silently coerced,
51
+ and reaches the asynchronous tier through `ExternalTier` if it needs to be shared at all.
52
+
53
+ ## Errors
54
+
55
+ | Error | Meaning |
56
+ | --- | --- |
57
+ | `StaleSnapshotError` | The version you held was reclaimed. Reacquire. |
58
+ | `ArenaCorruptError` | A decode found something a correct writer cannot produce. |
59
+ | `ArenaFullError` | The arena is exhausted and cannot grow. |
60
+ | `NoReaderSlotError` | Every reader slot is claimed. |
61
+ | `UnencodableValueError` | The value is outside the type ladder. |
62
+
63
+ ## Testing
64
+
65
+ ```bash
66
+ npm test # unit, property, and fail closed tests
67
+ npm run soak # multi process soak, the release gate for arena changes
68
+ npm run bench # the read latency harness
69
+ ```
@@ -0,0 +1,78 @@
1
+ import type { SharedArena } from "./arena.js";
2
+ /**
3
+ * Size class slabs above a bump region.
4
+ *
5
+ * Sizes are payload bytes, not counting the eight byte block header. They double roughly
6
+ * every two steps, which caps internal waste at about 25 percent while keeping the class
7
+ * count small enough that the lookup is a short scan rather than a division.
8
+ */
9
+ export declare const SIZE_CLASSES: readonly [8, 16, 24, 32, 48, 64, 96, 128, 192, 256, 384, 512, 768, 1024, 1536, 2048, 3072, 4096];
10
+ declare const MAX_CLASS_BYTES: number;
11
+ declare function classForSize(bytes: number): number;
12
+ export interface AllocatorStats {
13
+ readonly bumpPointer: number;
14
+ readonly capacityBytes: number;
15
+ readonly liveBytes: number;
16
+ readonly freeListBytes: number;
17
+ readonly allocations: number;
18
+ readonly frees: number;
19
+ readonly headerBytes: number;
20
+ }
21
+ /**
22
+ * The allocator runs only in the owner. That is the single biggest simplification in the
23
+ * design: there is exactly one writer, so no allocator structure needs a lock, and the free
24
+ * lists can live on the owner heap rather than in shared memory where a hostile window
25
+ * could corrupt them.
26
+ *
27
+ * Defend that property. Any feature that introduces a second writer costs a lock on every
28
+ * allocation, and the read path pays for it indirectly through longer commits.
29
+ */
30
+ export declare class Allocator {
31
+ #private;
32
+ constructor(arena: SharedArena, grow?: (minimumBytes: number) => boolean);
33
+ /**
34
+ * Allocate `byteSize` payload bytes and return the payload offset. The returned block is
35
+ * eight byte aligned and preceded by a validated header.
36
+ *
37
+ * The contents are not zeroed. Callers write every byte they later read, and the block
38
+ * header check plus the record specific length field is what makes a partially written
39
+ * block undecodable rather than plausible.
40
+ */
41
+ allocate(byteSize: number): number;
42
+ /**
43
+ * Return a block to its free list. Blocks larger than the largest size class are dropped
44
+ * rather than tracked, because a mixed size free list degrades into a first fit search
45
+ * and this allocator is not the place to pay for that. Phase 5 measures whether real
46
+ * workloads produce enough of them to need compaction.
47
+ */
48
+ free(offset: number): void;
49
+ stats(): AllocatorStats;
50
+ /**
51
+ * Rewind the bump pointer, discarding everything allocated above it.
52
+ *
53
+ * Called after a commit that failed part way, once its blocks have been freed. Freeing
54
+ * alone is not enough: the blocks go back to their size classes, so an arena that a failed
55
+ * write filled with sixteen byte records cannot then serve a forty byte request. There is
56
+ * no coalescing, so those small blocks never merge into a larger one and the store is stuck
57
+ * until it is restarted.
58
+ *
59
+ * Rewinding is safe here and nowhere else. There is exactly one writer, a commit is
60
+ * synchronous, and every block above the mark belongs to the commit being abandoned, so
61
+ * nothing published can reference one.
62
+ *
63
+ * The free lists are purged of anything above the mark, because a stale entry there would
64
+ * be handed out again by the free list while the bump allocator hands out the same bytes.
65
+ */
66
+ rewindTo(bumpPointer: number): void;
67
+ /**
68
+ * Bytes consumed from the arena that are neither live nor reusable.
69
+ *
70
+ * Block headers are excluded because they are fixed overhead rather than fragmentation.
71
+ * What remains is the blocks that were allocated at an exact size, freed, and dropped
72
+ * rather than tracked. A number that climbs with time is the signal that compaction is
73
+ * needed, which is the phase 5 question.
74
+ */
75
+ strandedBytes(): number;
76
+ }
77
+ export { MAX_CLASS_BYTES, classForSize };
78
+ //# sourceMappingURL=allocator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"allocator.d.ts","sourceRoot":"","sources":["../../src/allocator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAa9C;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,YACvB,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAC7E,CAAC;AAEX,QAAA,MAAM,eAAe,EAA4C,MAAM,CAAC;AAExE,iBAAS,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAK3C;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;;;GAQG;AACH,qBAAa,SAAS;;IAYpB,YAAY,KAAK,EAAE,WAAW,EAAE,IAAI,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,EAGvE;IAED;;;;;;;OAOG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAoBjC;IAED;;;;;OAKG;IACH,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CASzB;IAED,KAAK,IAAI,cAAc,CAUtB;IAED;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAoBlC;IAED;;;;;;;OAOG;IACH,aAAa,IAAI,MAAM,CAItB;CAgEF;AAED,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,CAAC"}
@@ -0,0 +1,211 @@
1
+ import { ArenaFullError } from "./errors.js";
2
+ import { BLOCK_HEADER_BYTES, BLOCK_MAGIC, BLOCK_MAGIC_SHIFT, Block, Header, SIZE_CLASS_EXACT, WORD, align, } from "./layout.js";
3
+ /**
4
+ * Size class slabs above a bump region.
5
+ *
6
+ * Sizes are payload bytes, not counting the eight byte block header. They double roughly
7
+ * every two steps, which caps internal waste at about 25 percent while keeping the class
8
+ * count small enough that the lookup is a short scan rather than a division.
9
+ */
10
+ export const SIZE_CLASSES = [
11
+ 8, 16, 24, 32, 48, 64, 96, 128, 192, 256, 384, 512, 768, 1024, 1536, 2048, 3072, 4096,
12
+ ];
13
+ const MAX_CLASS_BYTES = SIZE_CLASSES[SIZE_CLASSES.length - 1];
14
+ function classForSize(bytes) {
15
+ for (let index = 0; index < SIZE_CLASSES.length; index += 1) {
16
+ if (bytes <= SIZE_CLASSES[index])
17
+ return index;
18
+ }
19
+ return SIZE_CLASS_EXACT;
20
+ }
21
+ /**
22
+ * The allocator runs only in the owner. That is the single biggest simplification in the
23
+ * design: there is exactly one writer, so no allocator structure needs a lock, and the free
24
+ * lists can live on the owner heap rather than in shared memory where a hostile window
25
+ * could corrupt them.
26
+ *
27
+ * Defend that property. Any feature that introduces a second writer costs a lock on every
28
+ * allocation, and the read path pays for it indirectly through longer commits.
29
+ */
30
+ export class Allocator {
31
+ #arena;
32
+ /** One free list per size class, holding payload offsets of freed blocks. */
33
+ #freeLists = SIZE_CLASSES.map(() => []);
34
+ #liveBytes = 0;
35
+ #freeListBytes = 0;
36
+ /** Block header bytes consumed by bump allocation. Overhead, never reusable. */
37
+ #headerBytes = 0;
38
+ #allocations = 0;
39
+ #frees = 0;
40
+ #grow;
41
+ constructor(arena, grow) {
42
+ this.#arena = arena;
43
+ this.#grow = grow;
44
+ }
45
+ /**
46
+ * Allocate `byteSize` payload bytes and return the payload offset. The returned block is
47
+ * eight byte aligned and preceded by a validated header.
48
+ *
49
+ * The contents are not zeroed. Callers write every byte they later read, and the block
50
+ * header check plus the record specific length field is what makes a partially written
51
+ * block undecodable rather than plausible.
52
+ */
53
+ allocate(byteSize) {
54
+ if (byteSize <= 0)
55
+ throw new RangeError(`allocation size must be positive, got ${byteSize}`);
56
+ const sizeClass = classForSize(byteSize);
57
+ const blockBytes = sizeClass === SIZE_CLASS_EXACT
58
+ ? align(byteSize)
59
+ : SIZE_CLASSES[sizeClass];
60
+ if (sizeClass !== SIZE_CLASS_EXACT) {
61
+ const reused = this.#freeLists[sizeClass].pop();
62
+ if (reused !== undefined) {
63
+ this.#freeListBytes -= blockBytes;
64
+ this.#liveBytes += blockBytes;
65
+ this.#allocations += 1;
66
+ this.#writeBlockHeader(reused, sizeClass, blockBytes);
67
+ return reused;
68
+ }
69
+ }
70
+ return this.#bumpAllocate(blockBytes, sizeClass);
71
+ }
72
+ /**
73
+ * Return a block to its free list. Blocks larger than the largest size class are dropped
74
+ * rather than tracked, because a mixed size free list degrades into a first fit search
75
+ * and this allocator is not the place to pay for that. Phase 5 measures whether real
76
+ * workloads produce enough of them to need compaction.
77
+ */
78
+ free(offset) {
79
+ const byteSize = this.#arena.checkBlock(offset, "free");
80
+ const sizeClass = this.#arena.blockSizeClass(offset);
81
+ this.#liveBytes -= byteSize;
82
+ this.#frees += 1;
83
+ if (sizeClass === SIZE_CLASS_EXACT)
84
+ return;
85
+ this.#freeLists[sizeClass].push(offset);
86
+ this.#freeListBytes += byteSize;
87
+ }
88
+ stats() {
89
+ return {
90
+ bumpPointer: this.#arena.loadHeader(Header.BumpPointer),
91
+ capacityBytes: this.#arena.byteLength,
92
+ liveBytes: this.#liveBytes,
93
+ freeListBytes: this.#freeListBytes,
94
+ allocations: this.#allocations,
95
+ frees: this.#frees,
96
+ headerBytes: this.#headerBytes,
97
+ };
98
+ }
99
+ /**
100
+ * Rewind the bump pointer, discarding everything allocated above it.
101
+ *
102
+ * Called after a commit that failed part way, once its blocks have been freed. Freeing
103
+ * alone is not enough: the blocks go back to their size classes, so an arena that a failed
104
+ * write filled with sixteen byte records cannot then serve a forty byte request. There is
105
+ * no coalescing, so those small blocks never merge into a larger one and the store is stuck
106
+ * until it is restarted.
107
+ *
108
+ * Rewinding is safe here and nowhere else. There is exactly one writer, a commit is
109
+ * synchronous, and every block above the mark belongs to the commit being abandoned, so
110
+ * nothing published can reference one.
111
+ *
112
+ * The free lists are purged of anything above the mark, because a stale entry there would
113
+ * be handed out again by the free list while the bump allocator hands out the same bytes.
114
+ */
115
+ rewindTo(bumpPointer) {
116
+ const arena = this.#arena;
117
+ if (bumpPointer >= arena.loadHeader(Header.BumpPointer))
118
+ return;
119
+ for (let index = 0; index < this.#freeLists.length; index += 1) {
120
+ const list = this.#freeLists[index];
121
+ const kept = [];
122
+ for (const offset of list) {
123
+ if (offset - BLOCK_HEADER_BYTES >= bumpPointer) {
124
+ this.#freeListBytes -= SIZE_CLASSES[index];
125
+ // The block header above the mark goes away with the rewind.
126
+ this.#headerBytes -= BLOCK_HEADER_BYTES;
127
+ continue;
128
+ }
129
+ kept.push(offset);
130
+ }
131
+ this.#freeLists[index] = kept;
132
+ }
133
+ arena.storeHeader(Header.BumpPointer, bumpPointer);
134
+ }
135
+ /**
136
+ * Bytes consumed from the arena that are neither live nor reusable.
137
+ *
138
+ * Block headers are excluded because they are fixed overhead rather than fragmentation.
139
+ * What remains is the blocks that were allocated at an exact size, freed, and dropped
140
+ * rather than tracked. A number that climbs with time is the signal that compaction is
141
+ * needed, which is the phase 5 question.
142
+ */
143
+ strandedBytes() {
144
+ const bump = this.#arena.loadHeader(Header.BumpPointer);
145
+ const used = bump - this.#arena.geometry.arenaOffset;
146
+ return used - this.#liveBytes - this.#freeListBytes - this.#headerBytes;
147
+ }
148
+ #bumpAllocate(blockBytes, sizeClass) {
149
+ const arena = this.#arena;
150
+ const bump = arena.loadHeader(Header.BumpPointer);
151
+ const payloadOffset = bump + BLOCK_HEADER_BYTES;
152
+ const nextBump = payloadOffset + blockBytes;
153
+ if (nextBump > arena.byteLength) {
154
+ const needed = nextBump - arena.byteLength;
155
+ if (!this.#grow?.(needed)) {
156
+ // Out of fresh arena. Before giving up, take a block from a larger size class. It
157
+ // wastes the difference, and wasting some memory beats refusing a write when there is
158
+ // memory sitting on a list one class up.
159
+ //
160
+ // This runs only under exhaustion, so the common path is unchanged. Without it, a
161
+ // rejected write that freed a thousand forty eight byte blocks could not be followed
162
+ // by a write that needed sixteen, which makes one bad write brick the store.
163
+ const scavenged = this.#scavenge(sizeClass);
164
+ if (scavenged !== undefined)
165
+ return scavenged;
166
+ throw new ArenaFullError(blockBytes + BLOCK_HEADER_BYTES, arena.byteLength);
167
+ }
168
+ // Growth succeeded, so the bump pointer is still valid and the buffer is longer.
169
+ // Views do not track growth on their own, so pick up the longer one before retrying.
170
+ arena.refresh();
171
+ return this.#bumpAllocate(blockBytes, sizeClass);
172
+ }
173
+ arena.storeHeader(Header.BumpPointer, nextBump);
174
+ this.#headerBytes += BLOCK_HEADER_BYTES;
175
+ arena.storeHeader(Header.CapacityBytes, arena.byteLength);
176
+ this.#writeBlockHeader(payloadOffset, sizeClass, blockBytes);
177
+ this.#liveBytes += blockBytes;
178
+ this.#allocations += 1;
179
+ return payloadOffset;
180
+ }
181
+ /**
182
+ * Take a block from a larger size class.
183
+ *
184
+ * The block keeps its own header, so freeing it later returns it to the list it came from
185
+ * rather than to the smaller one it was lent to. The difference between the request and the
186
+ * block is wasted until then, which is why this is a last resort rather than a policy.
187
+ */
188
+ #scavenge(sizeClass) {
189
+ if (sizeClass === SIZE_CLASS_EXACT)
190
+ return undefined;
191
+ for (let index = sizeClass; index < SIZE_CLASSES.length; index += 1) {
192
+ const offset = this.#freeLists[index].pop();
193
+ if (offset === undefined)
194
+ continue;
195
+ const classBytes = SIZE_CLASSES[index];
196
+ this.#freeListBytes -= classBytes;
197
+ this.#liveBytes += classBytes;
198
+ this.#allocations += 1;
199
+ return offset;
200
+ }
201
+ return undefined;
202
+ }
203
+ #writeBlockHeader(payloadOffset, sizeClass, blockBytes) {
204
+ const words = this.#arena.words;
205
+ const base = payloadOffset / WORD;
206
+ words[base + Block.Header] = (BLOCK_MAGIC << BLOCK_MAGIC_SHIFT) | (sizeClass & 0xff);
207
+ words[base + Block.ByteSize] = blockBytes;
208
+ }
209
+ }
210
+ export { MAX_CLASS_BYTES, classForSize };
211
+ //# sourceMappingURL=allocator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"allocator.js","sourceRoot":"","sources":["../../src/allocator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,81 @@
1
+ import { type ArenaGeometry } from "./layout.js";
2
+ /**
3
+ * A typed view over one shared buffer, plus the bounds checks that make every dereference
4
+ * provably in range.
5
+ *
6
+ * Both the owner and every reader hold one of these. It carries no writer state, so it is
7
+ * safe to construct in a process that must never mutate the arena.
8
+ */
9
+ export declare class SharedArena {
10
+ #private;
11
+ readonly buffer: SharedArrayBuffer;
12
+ readonly geometry: ArenaGeometry;
13
+ private constructor();
14
+ /**
15
+ * Attach to a buffer an owner has already formatted. Validates the magic, the layout
16
+ * version, and the configuration checksum, so a reader never starts decoding a buffer it
17
+ * does not understand.
18
+ */
19
+ static attach(buffer: SharedArrayBuffer): SharedArena;
20
+ /** Format a fresh buffer. Only the owner calls this. */
21
+ static format(buffer: SharedArrayBuffer, options: {
22
+ maxReaders: number;
23
+ retainedCapacity: number;
24
+ flags: number;
25
+ }): SharedArena;
26
+ /**
27
+ * The cached views.
28
+ *
29
+ * These do not probe the buffer for growth. Probing here was measurably the most
30
+ * expensive thing on the read path: reading `byteLength` from a growable
31
+ * SharedArrayBuffer is not an inlined field load, and a single bounds checked decode
32
+ * touches these accessors half a dozen times.
33
+ *
34
+ * Growth is picked up by `refresh()` instead, which callers invoke at the one point
35
+ * where a longer view can matter: acquiring a version newer than the one they last saw.
36
+ * Anything a reader decodes through a pinned version was allocated before that version
37
+ * was published, so it is inside the view the reader had when it acquired.
38
+ */
39
+ get words(): Int32Array;
40
+ get floats(): Float64Array;
41
+ get units(): Uint16Array;
42
+ get bytes(): Uint8Array;
43
+ get byteLength(): number;
44
+ /**
45
+ * Header accessors read the cached view directly rather than going through the growth
46
+ * check in `words`.
47
+ *
48
+ * That is safe because the header sits at offset zero and its size is fixed, so it is
49
+ * inside every view this arena has ever had, including one made before a grow(). It is
50
+ * also worth doing: a read touches the header eight times, and routing each of those
51
+ * through a SharedArrayBuffer byteLength getter on a growable buffer cost more than the
52
+ * rest of the read path put together.
53
+ */
54
+ loadHeader(field: number): number;
55
+ storeHeader(field: number, value: number): void;
56
+ addHeader(field: number, delta: number): number;
57
+ /**
58
+ * Pick up a growth that another process performed. Callers that are about to read arena
59
+ * payload, rather than only the header, call this once rather than paying for the check
60
+ * on every access.
61
+ */
62
+ refresh(): void;
63
+ /** Bumped when a new owner adopts the buffer. A reader uses it to fail closed. */
64
+ get ownerGeneration(): number;
65
+ writeConfigChecksum(): void;
66
+ verifyConfigChecksum(): void;
67
+ /**
68
+ * Prove that `byteCount` bytes at `offset` lie inside the arena region and are aligned.
69
+ * Called before every dereference. A failure means the arena was written by something
70
+ * that is not a correct writer.
71
+ */
72
+ checkRange(offset: number, byteCount: number, what: string): void;
73
+ /**
74
+ * Validate the two word header preceding every allocation and return its payload size.
75
+ * This is the check that turns a wild offset into a typed error rather than a plausible
76
+ * looking value.
77
+ */
78
+ checkBlock(offset: number, what: string): number;
79
+ blockSizeClass(offset: number): number;
80
+ }
81
+ //# sourceMappingURL=arena.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"arena.d.ts","sourceRoot":"","sources":["../../src/arena.ts"],"names":[],"mappings":"AACA,OAAO,EAaL,KAAK,aAAa,EACnB,MAAM,aAAa,CAAC;AAGrB;;;;;;GAMG;AACH,qBAAa,WAAW;;IACtB,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACnC,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;IAQjC,OAAO,eAQN;IAED;;;;OAIG;IACH,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,iBAAiB,GAAG,WAAW,CA4BpD;IAED,wDAAwD;IACxD,MAAM,CAAC,MAAM,CACX,MAAM,EAAE,iBAAiB,EACzB,OAAO,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GACvE,WAAW,CAyBb;IAED;;;;;;;;;;;;OAYG;IACH,IAAI,KAAK,IAAI,UAAU,CAEtB;IAED,IAAI,MAAM,IAAI,YAAY,CAEzB;IAED,IAAI,KAAK,IAAI,WAAW,CAEvB;IAED,IAAI,KAAK,IAAI,UAAU,CAEtB;IAED,IAAI,UAAU,IAAI,MAAM,CAEvB;IAWD;;;;;;;;;OASG;IACH,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhC;IAED,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAE9C;IAED,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAE9C;IAED;;;;OAIG;IACH,OAAO,IAAI,IAAI,CAEd;IAED,kFAAkF;IAClF,IAAI,eAAe,IAAI,MAAM,CAE5B;IAED,mBAAmB,IAAI,IAAI,CAG1B;IAED,oBAAoB,IAAI,IAAI,CAU3B;IAWD;;;;OAIG;IACH,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAiBhE;IAED;;;;OAIG;IACH,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAsB/C;IAED,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAGrC;CACF"}