@pistonite/pure 0.25.0 → 0.26.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pistonite/pure",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "type": "module",
5
5
  "description": "Pure TypeScript libraries for my projects",
6
6
  "homepage": "https://github.com/Pistonite/pure",
@@ -0,0 +1,267 @@
1
+ import { describe, afterEach, expect, it } from "vitest";
2
+
3
+ import { type Erc, makeErcType } from "./erc.ts";
4
+
5
+ type Rc = {
6
+ value: string;
7
+ refCount: number;
8
+ };
9
+ const Marker = Symbol("test");
10
+ type Marker = typeof Marker;
11
+ class Allocator {
12
+ private mockMemory: (Rc | undefined)[] = [];
13
+ public makeTestErc: (ptr: number) => Erc<Marker>;
14
+
15
+ // double indirection means each Erc holds a pointer to the external smart pointer,
16
+ // which also means external smart pointers all need to be heap allocated
17
+ // addRef() will return a pointer to a new external smart pointer
18
+ //
19
+ // single indirection on the other hand, means each Erc holds a pointer
20
+ // to the object directly. and addRef() will return the same pointer
21
+ //
22
+ // The Erc implementation must work with both
23
+ constructor(isDoubleIndirection: boolean) {
24
+ this.makeTestErc = makeErcType({
25
+ marker: Marker,
26
+ free: (ptr: number) => {
27
+ if (ptr < 0 || ptr >= this.mockMemory.length) {
28
+ throw new Error(
29
+ `Invalid index into mock memory. Length is ${this.mockMemory.length} but index is ${ptr}`,
30
+ );
31
+ }
32
+
33
+ const rc = this.mockMemory[ptr];
34
+ if (!rc) {
35
+ throw new Error("Double free detected");
36
+ }
37
+
38
+ rc.refCount--;
39
+ if (isDoubleIndirection) {
40
+ this.mockMemory[ptr] = undefined;
41
+ return;
42
+ }
43
+
44
+ if (rc.refCount === 0) {
45
+ this.mockMemory[ptr] = undefined;
46
+ }
47
+ },
48
+ addRef: (value: number) => {
49
+ if (value < 0 || value >= this.mockMemory.length) {
50
+ throw new Error(
51
+ `Invalid index into mock memory. Length is ${this.mockMemory.length} but index is ${value}`,
52
+ );
53
+ }
54
+
55
+ const rc = this.mockMemory[value];
56
+ if (!rc) {
57
+ throw new Error("AddRef on freed memory detected");
58
+ }
59
+ rc.refCount++;
60
+ if (isDoubleIndirection) {
61
+ for (let i = 0; i < this.mockMemory.length; i++) {
62
+ if (this.mockMemory[i] === undefined) {
63
+ this.mockMemory[i] = rc;
64
+ return i;
65
+ }
66
+ }
67
+ this.mockMemory.push(rc);
68
+ return this.mockMemory.length - 1;
69
+ }
70
+
71
+ return value;
72
+ },
73
+ });
74
+ }
75
+
76
+ allocValue(value: string): number {
77
+ const rc: Rc = {
78
+ value,
79
+ refCount: 1,
80
+ };
81
+ for (let i = 0; i < this.mockMemory.length; i++) {
82
+ if (this.mockMemory[i] === undefined) {
83
+ this.mockMemory[i] = rc;
84
+ return i;
85
+ }
86
+ }
87
+ this.mockMemory.push(rc);
88
+ return this.mockMemory.length - 1;
89
+ }
90
+
91
+ getValue(ptr: number | undefined): string {
92
+ if (ptr === undefined) {
93
+ throw new Error("Dereference of nullptr");
94
+ }
95
+ if (ptr < 0 || ptr >= this.mockMemory.length) {
96
+ throw new Error(
97
+ `Invalid index into mock memory. Length is ${this.mockMemory.length} but index is ${ptr}`,
98
+ );
99
+ }
100
+
101
+ const rc = this.mockMemory[ptr];
102
+ if (!rc) {
103
+ throw new Error("Dangling pointer");
104
+ }
105
+ return rc.value;
106
+ }
107
+
108
+ expectNoLeak(): void {
109
+ for (let i = 0; i < this.mockMemory.length; i++) {
110
+ const rc = this.mockMemory[i];
111
+ if (rc) {
112
+ throw new Error(
113
+ `Memory leak detected at index ${i}. Value: ${rc.value}, RefCount: ${rc.refCount}`,
114
+ );
115
+ }
116
+ }
117
+ }
118
+
119
+ cleanup(): void {
120
+ this.mockMemory = [];
121
+ }
122
+ }
123
+
124
+ describe.each`
125
+ indirection | allocator
126
+ ${"single"} | ${new Allocator(false)}
127
+ ${"double"} | ${new Allocator(true)}
128
+ `(
129
+ "Erc - $indirection indirection",
130
+ ({ allocator }: { allocator: Allocator }) => {
131
+ afterEach(() => {
132
+ allocator.cleanup();
133
+ });
134
+
135
+ it("allocate and deallocate correctly", () => {
136
+ const test = allocator.makeTestErc(allocator.allocValue("Hello"));
137
+ expect(allocator.getValue(test.value)).toBe("Hello");
138
+ test.free();
139
+ allocator.expectNoLeak();
140
+ });
141
+
142
+ it("frees if assigned new value", () => {
143
+ const test = allocator.makeTestErc(allocator.allocValue("Hello"));
144
+ test.assign(allocator.allocValue("World"));
145
+ expect(allocator.getValue(test.value)).toBe("World");
146
+ test.free();
147
+ allocator.expectNoLeak();
148
+ });
149
+
150
+ it("does not free when taking value", () => {
151
+ const test = allocator.makeTestErc(allocator.allocValue("Hello"));
152
+ const raw = test.take();
153
+ expect(allocator.getValue(raw)).toBe("Hello");
154
+ expect(test.value).toBeUndefined();
155
+ if (raw === undefined) {
156
+ throw new Error("Raw value is undefined");
157
+ }
158
+ allocator.makeTestErc(raw).free();
159
+ allocator.expectNoLeak();
160
+ });
161
+
162
+ it("invalidates weak references on free", () => {
163
+ const test = allocator.makeTestErc(allocator.allocValue("Hello"));
164
+ const testWeak = test.getWeak();
165
+ expect(allocator.getValue(testWeak.value)).toBe("Hello");
166
+ test.free();
167
+ expect(testWeak.value).toBeUndefined();
168
+ allocator.expectNoLeak();
169
+ });
170
+
171
+ it("invalidates weak references on assign", () => {
172
+ const test = allocator.makeTestErc(allocator.allocValue("Hello"));
173
+ const testWeak = test.getWeak();
174
+ expect(allocator.getValue(testWeak.value)).toBe("Hello");
175
+ test.assign(allocator.allocValue("World"));
176
+ expect(testWeak.value).toBeUndefined();
177
+ test.free();
178
+ allocator.expectNoLeak();
179
+ });
180
+
181
+ it("handles assign and take of different references correctly", () => {
182
+ const test1 = allocator.makeTestErc(allocator.allocValue("Hello"));
183
+ const test2 = allocator.makeTestErc(allocator.allocValue("World"));
184
+ expect(allocator.getValue(test1.value)).toBe("Hello");
185
+ expect(allocator.getValue(test2.value)).toBe("World");
186
+ test1.assign(test2.take());
187
+ expect(allocator.getValue(test1.value)).toBe("World");
188
+ test1.free();
189
+ allocator.expectNoLeak();
190
+ });
191
+
192
+ it("handles assign and take of same references correctly", () => {
193
+ const test1 = allocator.makeTestErc(allocator.allocValue("Hello"));
194
+ const test2 = test1.getStrong();
195
+ test1.assign(test2.take());
196
+ expect(allocator.getValue(test1.value)).toBe("Hello");
197
+ test1.free();
198
+ test2.free(); // should be no-op
199
+ allocator.expectNoLeak();
200
+ });
201
+
202
+ it("assigning another Erc directly should cause double free", async () => {
203
+ const test1 = allocator.makeTestErc(allocator.allocValue("Hello"));
204
+ const test2 = test1.getStrong();
205
+ test1.assign(test2.value);
206
+ expect(allocator.getValue(test1.value)).toBe("Hello");
207
+ test1.free();
208
+
209
+ const freeTest2 = async () => {
210
+ test2.free();
211
+ };
212
+ await expect(freeTest2).rejects.toThrow("Double free detected");
213
+ allocator.expectNoLeak();
214
+ });
215
+
216
+ it("handles assign and take of same references correctly (same Erc)", () => {
217
+ const test1 = allocator.makeTestErc(allocator.allocValue("Hello"));
218
+ test1.assign(test1.take());
219
+ expect(allocator.getValue(test1.value)).toBe("Hello");
220
+ test1.free();
221
+ allocator.expectNoLeak();
222
+ });
223
+
224
+ it("assigning another Erc directly should cause double free (same Erc)", async () => {
225
+ const test1 = allocator.makeTestErc(allocator.allocValue("Hello"));
226
+ test1.assign(test1.value);
227
+ const getTest1Value = async () => {
228
+ allocator.getValue(test1.value);
229
+ };
230
+ await expect(getTest1Value).rejects.toThrow("Dangling pointer");
231
+
232
+ const freeTest1 = async () => {
233
+ test1.free();
234
+ };
235
+ await expect(freeTest1).rejects.toThrow("Double free detected");
236
+ allocator.expectNoLeak();
237
+ });
238
+
239
+ it("inc ref count with strong reference", () => {
240
+ const test = allocator.makeTestErc(allocator.allocValue("Hello"));
241
+ const test2 = test.getStrong();
242
+ expect(allocator.getValue(test.value)).toBe("Hello");
243
+ expect(allocator.getValue(test2.value)).toBe("Hello");
244
+ test.free();
245
+ expect(allocator.getValue(test2.value)).toBe("Hello");
246
+ test2.free();
247
+ allocator.expectNoLeak();
248
+ });
249
+
250
+ it("inc ref count with strong reference from weak reference", () => {
251
+ const test = allocator.makeTestErc(allocator.allocValue("Hello"));
252
+ const testWeak = test.getWeak();
253
+ expect(allocator.getValue(testWeak.value)).toBe("Hello");
254
+ const test2 = testWeak.getStrong();
255
+ expect(allocator.getValue(testWeak.value)).toBe("Hello");
256
+ expect(allocator.getValue(test2.value)).toBe("Hello");
257
+ const test2Weak = test2.getWeak();
258
+ test.free();
259
+ expect(testWeak.value).toBeUndefined();
260
+ expect(allocator.getValue(test2.value)).toBe("Hello");
261
+ expect(allocator.getValue(test2Weak.value)).toBe("Hello");
262
+ test2.free();
263
+ expect(test2Weak.value).toBeUndefined();
264
+ allocator.expectNoLeak();
265
+ });
266
+ },
267
+ );
@@ -0,0 +1,316 @@
1
+ /**
2
+ * A holder for an externally ref-counted object.
3
+ *
4
+ * See {@link makeErcType} for how to use
5
+ */
6
+ export type Erc<TName, TRepr = number> = {
7
+ readonly type: TName;
8
+
9
+ /**
10
+ * Underlying object representation.
11
+ *
12
+ * The repr should not be undefinable. undefined means nullptr
13
+ */
14
+ readonly value: TRepr | undefined;
15
+
16
+ /**
17
+ * Free the underlying object.
18
+ *
19
+ * All weak references will be invalidated, and this Erc will become
20
+ * empty
21
+ */
22
+ free: () => void;
23
+
24
+ /**
25
+ * Assign a new value to this Erc.
26
+ *
27
+ * The old value will be freed, and all weak references will be invalidated.
28
+ */
29
+ assign: (value: TRepr | undefined) => void;
30
+
31
+ /**
32
+ * Take the inner value without freeing it.
33
+ *
34
+ * All weak references will be invalidated, and this Erc will become
35
+ * empty
36
+ */
37
+ take: () => TRepr | undefined;
38
+
39
+ /**
40
+ * Create a weak reference to the inner value.
41
+ *
42
+ * When this Erc is freed, all weak references will be invalidated.
43
+ */
44
+ getWeak: () => ErcRef<TName, TRepr>;
45
+
46
+ /**
47
+ * Create a strong reference to the inner value, essentially
48
+ * incrementing the ref count.
49
+ */
50
+ getStrong: () => Erc<TName, TRepr>;
51
+ };
52
+
53
+ /**
54
+ * Weak reference to an externally ref-counted object.
55
+ *
56
+ * See {@link makeErcType} for how to use
57
+ */
58
+ export type ErcRef<TName, TRepr = number> = {
59
+ readonly type: TName;
60
+
61
+ /**
62
+ * The underlying object representation.
63
+ *
64
+ * This may become undefined across async calls if the weak reference
65
+ * is invalidated
66
+ */
67
+ readonly value: TRepr | undefined;
68
+
69
+ /**
70
+ * Create a strong reference to the inner value, essentially
71
+ * incrementing the ref count.
72
+ */
73
+ getStrong: () => Erc<TName, TRepr>;
74
+ };
75
+
76
+ export type ErcRefType<T> =
77
+ T extends Erc<infer TName, infer TRepr> ? ErcRef<TName, TRepr> : never;
78
+
79
+ export type ErcTypeConstructor<TName, TRepr> = {
80
+ /**
81
+ * A marker value for the underlying object type.
82
+ *
83
+ * This is commonly a string literal or a symbol.
84
+ */
85
+ marker: TName;
86
+
87
+ /**
88
+ * The function to free the underlying object.
89
+ */
90
+ free: (value: TRepr) => void;
91
+
92
+ /**
93
+ * Given a value, increase the ref count and return the new reference.
94
+ * The returned representation should be a different value if double indirection
95
+ * is used (each value is a pointer to the smart pointer), or the same value
96
+ * if single indirection is used (the value is pointing to the object itself).
97
+ */
98
+ addRef: (value: TRepr) => TRepr;
99
+ };
100
+
101
+ /**
102
+ * Create a constructor function that serves as the type for an externally ref-counted
103
+ * object type. Erc instances can then be created for manually managing memory for
104
+ * external objects, typically through FFI.
105
+ *
106
+ * Since JS is garbage collected and has no way to enforce certain memory management,
107
+ * the programmer must ensure that Erc instances are handled correctly!!!
108
+ *
109
+ * ## Defining Erc type
110
+ * ```typescript
111
+ * import { makeErcType, type Erc } from "@pistonite/pure/memory";
112
+ *
113
+ * // 2 functions are required to create an Erc type:
114
+ * // free: free the underlying value (essentially decrementing the ref count)
115
+ * // addRef: increment the ref count and return the new reference
116
+ *
117
+ * // here, assume `number` is the JS type used to represent the external object
118
+ * // for example, this can be a pointer to a C++ object
119
+ * declare function freeFoo(obj: number) => void;
120
+ * declare function addRefFoo(obj: number) => number;
121
+ *
122
+ * // assume another function to create (allocate) the object
123
+ * declare function createFoo(): number;
124
+ *
125
+ * // The recommended way is to create a unique symbol for tracking
126
+ * // the external type, you can also use a string literal
127
+ * const Foo = Symbol("Foo");
128
+ * type Foo = typeof Foo;
129
+ *
130
+ * // now, we can create the Erc type
131
+ * const makeFooErc = makeErcType({
132
+ * marker: Foo,
133
+ * free: freeFoo,
134
+ * addRef: addRefFoo,
135
+ * });
136
+ *
137
+ * // and create Erc instances
138
+ * const myFoo: Erc<Foo> = makeFooErc(createFoo());
139
+ * ```
140
+ *
141
+ * ## Using Erc (strong reference)
142
+ * Each `Erc` instance is a strong reference, corresponding to some ref-counted
143
+ * object externally. Therefore, owner of the `Erc` instance should
144
+ * not expose the `Erc` instance to others (for example, returning it from a function),
145
+ * since it will lead to memory leak or double free.
146
+ * ```typescript
147
+ * // create a foo instance externally, and wrap it with Erc
148
+ * const myFoo = makeFooErc(createFoo());
149
+ *
150
+ * // if ownership of myFoo should be returned to external, use `take()`
151
+ * // this will make myFoo empty, and doSomethingWithFooExternally should free it
152
+ * doSomethingWithFooExternally(myFoo.take());
153
+ *
154
+ * // you can also free it directly
155
+ * foo.free();
156
+ * ```
157
+ *
158
+ * ## Using ErcRef (weak reference)
159
+ * Calling `getWeak` on an `Erc` will return a weak reference that has the same
160
+ * inner value. The weak reference is safe to be passed around and copied.
161
+ * ```typescript
162
+ * const myFooWeak = myFoo.getWeak();
163
+ * ```
164
+ *
165
+ * The weak references are tracked by the `Erc` instance.
166
+ * In the example above, when `myFoo` is freed, all weak references created by
167
+ * `getWeak` will be invalidated. If some other code kept the inner value of
168
+ * the weak reference, it will become a dangling pointer.
169
+ *
170
+ * To avoid this, `getStrong` can be used to create a strong reference if
171
+ * the weak reference is still valid, to ensure that the underlying object
172
+ * is never freed while still in use
173
+ * ```typescript
174
+ * const myFooWeak = myFoo.getWeak();
175
+ *
176
+ * // assume we have some async code that needs to use myFoo
177
+ * declare async function doSomethingWithFoo(foo: FooErcRef): Promise<void>;
178
+ *
179
+ * // Below is BAD!
180
+ * await doSomethingWithFoo(myFooWeak);
181
+ * // Reason: doSomethingWithFoo is async, so it's possible that
182
+ * // myFooWeak is invalidated when it's still needed. If the implementation
183
+ * // does not check for that, it could be deferencing a dangling pointer
184
+ * // (of course, it could actually be fine depending on the implementation of doSomethingWithFoo)
185
+ *
186
+ * // Recommendation is to use strong reference for async operations
187
+ * const myFooStrong = myFooWeak.getStrong();
188
+ * await doSomethingWithFoo(myFooStrong.getWeak()); // will never be freed while awaiting
189
+ * // now we free
190
+ * myFooStrong.free();
191
+ *
192
+ * ```
193
+ *
194
+ * ## Assigning to Erc
195
+ * Each `Erc` instance should only ever have one external reference. So you should not
196
+ * assign to an `Erc` variable directly:
197
+ * ```typescript
198
+ * // DO NOT DO THIS
199
+ * let myFoo = makeFooErc(createFoo());
200
+ * myFoo = makeFooErc(createFoo()); // previous Erc is overriden without proper clean up
201
+ * ```
202
+ *
203
+ * If you want to attach a new value to an existing `Erc`, use the `assign` method:
204
+ * ```typescript
205
+ * const myFoo = makeFooErc(createFoo());
206
+ * myFoo.assign(createFoo()); // previous Erc is freed, and the new one is assigned
207
+ * myFoo.free(); // new one is freed
208
+ * ```
209
+ * The example above does not cause leaks, since the previous Erc is freed
210
+ *
211
+ * However, if you call assign with the value of another `Erc`, it will cause memory
212
+ * issues:
213
+ * ```typescript
214
+ * // DO NOT DO THIS
215
+ * const myFoo1 = makeFooErc(createFoo());
216
+ * const myFoo2 = makeFooErc(createFoo());
217
+ * myFoo1.assign(myFoo2.value); // myFoo1 is freed, and myFoo2 is assigned
218
+ * // BAD: now both myFoo1 and myFoo2 references the same object, but the ref count is 1
219
+ * myFoo1.free(); // no issue here, object is freed, but myFoo2 now holds a dangling pointer
220
+ * myFoo2.free(); // double free!
221
+ *
222
+ * // The correct way to do this is to use `take`:
223
+ * const myFoo1 = makeFooErc(createFoo());
224
+ * const myFoo2 = makeFooErc(createFoo());
225
+ * myFoo1.assign(myFoo2.take()); // myFoo1 is freed, and myFoo2 is assigned, myFoo2 is empty
226
+ * myFoo1.free(); // no issue here, object is freed
227
+ * // myFoo2 is empty, so calling free() has no effect
228
+ * ```
229
+ *
230
+ * Assign also works if both `Erc` are 2 references of the same object:
231
+ * ```typescript
232
+ * const myFoo1 = makeFooErc(createFoo()); // ref count is 1
233
+ * const myFoo2 = myFoo1.getStrong(); // ref count is 2
234
+ * myFoo1.assign(myFoo2.take()); // frees old value, ref count is 1
235
+ *
236
+ * // This also works:
237
+ * const myFoo1 = makeFooErc(createFoo()); // ref count is 1
238
+ * myFoo1.assign(myFoo1.take()); // take() makes myFoo1 empty, so assign() doesn't free it
239
+ *
240
+ * // DO NOT DO THIS:
241
+ * myFoo1.assign(myFoo1.value); // this will free the value since ref count is 0, and result in a dangling pointer
242
+ * ```
243
+ *
244
+ */
245
+ export const makeErcType = <TName, TRepr>({
246
+ marker,
247
+ free,
248
+ addRef,
249
+ }: ErcTypeConstructor<TName, TRepr>): ((
250
+ value: TRepr | undefined,
251
+ ) => Erc<TName, TRepr>) => {
252
+ const createStrongRef = (value: TRepr | undefined): Erc<TName, TRepr> => {
253
+ let weakRef:
254
+ | (ErcRef<TName, TRepr> & { invalidate: () => void })
255
+ | undefined = undefined;
256
+ const invalidateWeakRef = () => {
257
+ if (!weakRef) {
258
+ return;
259
+ }
260
+ const oldWeakRef = weakRef;
261
+ weakRef = undefined;
262
+ oldWeakRef.invalidate();
263
+ };
264
+ const createWeakRef = (initialValue: TRepr | undefined) => {
265
+ const weak = {
266
+ type: marker,
267
+ value: initialValue,
268
+ invalidate: () => {
269
+ weak.value = undefined;
270
+ },
271
+ getStrong: () => {
272
+ if (weak.value === undefined) {
273
+ return createStrongRef(undefined);
274
+ }
275
+ return createStrongRef(addRef(weak.value));
276
+ },
277
+ };
278
+ return weak;
279
+ };
280
+ const erc = {
281
+ type: marker,
282
+ value,
283
+ free: () => {
284
+ if (erc.value !== undefined) {
285
+ invalidateWeakRef();
286
+ free(erc.value);
287
+ erc.value = undefined;
288
+ }
289
+ },
290
+ assign: (newValue: TRepr | undefined) => {
291
+ erc.free();
292
+ erc.value = newValue;
293
+ },
294
+ take: () => {
295
+ invalidateWeakRef();
296
+ const oldValue = erc.value;
297
+ erc.value = undefined;
298
+ return oldValue;
299
+ },
300
+ getWeak: () => {
301
+ if (!weakRef) {
302
+ weakRef = createWeakRef(erc.value);
303
+ }
304
+ return weakRef;
305
+ },
306
+ getStrong: () => {
307
+ if (erc.value === undefined) {
308
+ return createStrongRef(undefined);
309
+ }
310
+ return createStrongRef(addRef(erc.value));
311
+ },
312
+ };
313
+ return erc;
314
+ };
315
+ return createStrongRef;
316
+ };
@@ -6,4 +6,4 @@
6
6
  */
7
7
  export { cell, type CellConstructor, type Cell } from "./cell.ts";
8
8
  export { persist, type PersistConstructor, type Persist } from "./persist.ts";
9
- export * from "./weak.ts";
9
+ export * from "./erc.ts";
@@ -1,140 +0,0 @@
1
- export type ExternalWeakRef<TUnderlying, TType> = {
2
- /**
3
- * A marker value for the underlying object type.
4
- *
5
- * This is commonly a string literal or a symbol.
6
- */
7
- type: TType;
8
-
9
- /**
10
- * The underlying object reference.
11
- */
12
- ref: TUnderlying | undefined;
13
-
14
- /**
15
- * Free the underlying object.
16
- */
17
- free: () => void;
18
-
19
- /**
20
- * Update the underlying object reference.
21
- *
22
- * If the new reference is the same as the old one, nothing will happen.
23
- * If the old reference is not undefined, it will be freed.
24
- */
25
- set: (value: TUnderlying | undefined) => void;
26
- };
27
-
28
- export type ExternalWeakRefConstructor<TUnderlying, TType> = {
29
- /**
30
- * A marker value for the underlying object type.
31
- *
32
- * This is commonly a string literal or a symbol.
33
- */
34
- marker: TType;
35
-
36
- /**
37
- * The function to free the underlying object.
38
- */
39
- free: (obj: TUnderlying) => void;
40
- };
41
-
42
- /**
43
- * Create a weak reference type for managing externally memory-managed object. This means
44
- * the objects needs to be freed manually by the external code.
45
- *
46
- * The `marker` option is used to distinguish between different types of weak references
47
- * with the same underlying representation for the reference.
48
- *
49
- * Note that the underlying representation should not be undefined-able!
50
- *
51
- * ## Example
52
- * ```typescript
53
- * import { makeExternalWeakRefType } from "@pistonite/pure/memory";
54
- *
55
- * // assume `number` is the JS type used to represent the external object
56
- * // for example, this can be a pointer to a C++ object
57
- * declare function freeFoo(obj: number) => void;
58
- *
59
- * // some function that allocates a foo object externally and returns
60
- * // a reference
61
- * declare function getFoo(): number;
62
- *
63
- * const makeFooRef = makeExternalWeakRefType({
64
- * marker: "foo",
65
- * free: (obj) => {
66
- * freeFoo(obj);
67
- * }
68
- * });
69
- * type FooRef = ReturnType<typeof makeFooRef>;
70
- *
71
- * // create a reference to a foo object
72
- * // now this reference can be passed around in JS,
73
- * // as long as the ownership model is clear and the owner
74
- * // remembers to free it
75
- * const fooRef = makeFooRef(getFoo());
76
- *
77
- * // free the foo object when it is no longer needed
78
- * fooRef.free();
79
- *
80
- * ## Updating the reference
81
- * The `set` method will update the reference and free the old one if exists
82
- * ```
83
- * const fooRef = makeFooRef(getFoo());
84
- * fooRef.set(getFoo()); // the old one will be freed, unless it is the same as the new one
85
- * ```
86
- *
87
- * This has a major pitfall: If the ExternalWeakRef is shared, the new object will be accessible
88
- * by code that has the old reference. In other words, when the reference is updated, code that
89
- * already has the old reference will not able to know that it has changed.
90
- *
91
- * If this is a problem, you should use this pattern instead:
92
- * ```typescript
93
- * // track the "current" valid reference
94
- * let currentRef = makeFooRef(undefined);
95
- *
96
- * export const getFooRef = (): FooRef => {
97
- * // because of this function, many other places can hold
98
- * // a valid reference to foo
99
- * return currentRef;
100
- * }
101
- *
102
- * export const updateFooRef = (newFoo: number): void => {
103
- * // when updating the reference, we create a new weak ref and free the old one
104
- * if (currentRef.ref === newFoo) {
105
- * return; // always need to check if old and new are the same, otherwise we will be freeing the new one
106
- * }
107
- * const newRef = makeFooRef(newFoo);
108
- * currentRef.free();
109
- * currentRef = newRef;
110
- *
111
- * // now other places that hold the old reference will see it's freed
112
- * }
113
- * ```
114
- */
115
- export const makeExternalWeakRefType = <TUnderlying, TType>({
116
- marker,
117
- free,
118
- }: ExternalWeakRefConstructor<TUnderlying, TType>) => {
119
- return (
120
- obj: TUnderlying | undefined,
121
- ): ExternalWeakRef<TUnderlying, TType> => {
122
- const weakRefObj = {
123
- type: marker,
124
- ref: obj,
125
- free: () => {
126
- if (weakRefObj.ref !== undefined) {
127
- free(weakRefObj.ref);
128
- }
129
- weakRefObj.ref = undefined;
130
- },
131
- set: (value: TUnderlying | undefined) => {
132
- if (weakRefObj.ref !== undefined && weakRefObj.ref !== value) {
133
- free(weakRefObj.ref);
134
- }
135
- weakRefObj.ref = value;
136
- },
137
- };
138
- return weakRefObj;
139
- };
140
- };