@isograph/reference-counted-pointer 0.0.0-main-4ef7c123

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 ADDED
@@ -0,0 +1,7 @@
1
+ # Reference counted pointers
2
+
3
+ ## API
4
+
5
+ ```js
6
+
7
+ ```
@@ -0,0 +1,33 @@
1
+ import type { ItemCleanupPair } from "@isograph/disposable-types";
2
+ /**
3
+ * Create an undisposed reference-counted pointer guarding a given item.
4
+ *
5
+ * Once all reference-counted pointers guarding a given item have been
6
+ * disposed, the underlying item will be disposed.
7
+ *
8
+ * Additional reference-counted pointers guarding the same item can be
9
+ * created by calling retainIfNotDisposed().
10
+ *
11
+ * ## Structural sharing
12
+ *
13
+ * Reference counted pointers enable reusing disposable items between
14
+ * application states, so-called structural sharing.
15
+ *
16
+ * If state 1 contains a reference counted pointer to an item, in order
17
+ * to transition to state 2, one would first create an additional
18
+ * reference-counted pointer by calling cloneIfNotDisposed, transition
19
+ * to state 2, then clean up state 1 by disposing of its reference-
20
+ * counted pointers. In this transition, at no time were there zero
21
+ * undisposed reference countend pointers to the disposable item, so it
22
+ * was never disposed, and we could reuse it between states.
23
+ */
24
+ export declare function createReferenceCountedPointer<T>(pair: ItemCleanupPair<T>): ItemCleanupPair<ReferenceCountedPointer<T>>;
25
+ export interface ReferenceCountedPointer<T> {
26
+ isDisposed(): boolean;
27
+ /**
28
+ * Safety: the item returned here is valid for use only as long as the reference
29
+ * counted pointer is not disposed.
30
+ */
31
+ getItemIfNotDisposed(): T | null;
32
+ cloneIfNotDisposed(): ItemCleanupPair<ReferenceCountedPointer<T>> | null;
33
+ }
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createReferenceCountedPointer = void 0;
4
+ // TODO cloneIfNotDisposed should also return the underlying item
5
+ /**
6
+ * Create an undisposed reference-counted pointer guarding a given item.
7
+ *
8
+ * Once all reference-counted pointers guarding a given item have been
9
+ * disposed, the underlying item will be disposed.
10
+ *
11
+ * Additional reference-counted pointers guarding the same item can be
12
+ * created by calling retainIfNotDisposed().
13
+ *
14
+ * ## Structural sharing
15
+ *
16
+ * Reference counted pointers enable reusing disposable items between
17
+ * application states, so-called structural sharing.
18
+ *
19
+ * If state 1 contains a reference counted pointer to an item, in order
20
+ * to transition to state 2, one would first create an additional
21
+ * reference-counted pointer by calling cloneIfNotDisposed, transition
22
+ * to state 2, then clean up state 1 by disposing of its reference-
23
+ * counted pointers. In this transition, at no time were there zero
24
+ * undisposed reference countend pointers to the disposable item, so it
25
+ * was never disposed, and we could reuse it between states.
26
+ */
27
+ function createReferenceCountedPointer(pair) {
28
+ const originalReferenceCountedPointer = new RefCounter(pair);
29
+ return originalReferenceCountedPointer.retainIfNotDisposed();
30
+ }
31
+ exports.createReferenceCountedPointer = createReferenceCountedPointer;
32
+ // N.B. this could implement ReferenceCountedPointer<T>, but it would not be correct to use it
33
+ // as such, since it does not have an associated dispose function that can be called.
34
+ //
35
+ // Note that there is no way, and should be no way, to determine whether the underlying item
36
+ // has been disposed, let alone force it to be disposed! If you need that, you need to keep track
37
+ // of all calls to retainIfNotDisposed.
38
+ class RefCounter {
39
+ /**
40
+ * Private. Do not expose this class directly, as this contructor creates a ReferenceCountedPointer
41
+ * in an invalid state. We must immediately, after creation, call retainIfNotDisposed().
42
+ */
43
+ constructor([item, dispose]) {
44
+ this.__state = {
45
+ item,
46
+ dispose,
47
+ activeReferenceCount: 0,
48
+ };
49
+ }
50
+ getIfNotDisposed() {
51
+ return this.__state === null ? null : this.__state.item;
52
+ }
53
+ retainIfNotDisposed() {
54
+ if (this.__state !== null) {
55
+ this.__state.activeReferenceCount++;
56
+ const activeReference = new ActiveReference(this);
57
+ let disposed = false;
58
+ const dispose = () => {
59
+ if (disposed) {
60
+ throw new Error("Do not dispose an already-disposed ActiveReference.");
61
+ }
62
+ disposed = true;
63
+ if (activeReference.__original === null) {
64
+ throw new Error("Attempted to dispose an active reference, but it was already disposed. " +
65
+ "This indicates a bug in reference-counted-pointer.");
66
+ }
67
+ activeReference.__original = null;
68
+ if (this.__state === null) {
69
+ throw new Error("Attempted to dispose, but the underlying reference counted pointer was disposed. " +
70
+ "This indicates a bug in reference-counted-pointer.");
71
+ }
72
+ this.__state.activeReferenceCount--;
73
+ this.__maybeDispose();
74
+ };
75
+ return [activeReference, dispose];
76
+ }
77
+ else {
78
+ return null;
79
+ }
80
+ }
81
+ __maybeDispose() {
82
+ if (this.__state === null) {
83
+ throw new Error("__maybeDispose was called, but the reference counted pointer was disposed. " +
84
+ "This indicates a bug in reference-counted-pointer.");
85
+ }
86
+ if (this.__state.activeReferenceCount === 0) {
87
+ this.__state.dispose();
88
+ this.__state = null;
89
+ }
90
+ }
91
+ }
92
+ class ActiveReference {
93
+ constructor(original) {
94
+ this.__original = original;
95
+ }
96
+ isDisposed() {
97
+ return this.__original === null;
98
+ }
99
+ cloneIfNotDisposed() {
100
+ var _a, _b;
101
+ return (_b = (_a = this.__original) === null || _a === void 0 ? void 0 : _a.retainIfNotDisposed()) !== null && _b !== void 0 ? _b : null;
102
+ }
103
+ getItemIfNotDisposed() {
104
+ var _a, _b;
105
+ return (_b = (_a = this.__original) === null || _a === void 0 ? void 0 : _a.getIfNotDisposed()) !== null && _b !== void 0 ? _b : null;
106
+ }
107
+ }
@@ -0,0 +1 @@
1
+ export * from "./createReferenceCountedPointer";
package/dist/index.js ADDED
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./createReferenceCountedPointer"), exports);
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@isograph/reference-counted-pointer",
3
+ "version": "0.0.0-main-4ef7c123",
4
+ "description": "Reference counted pointers enable sharing of disposable items.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "author": "Isograph Labs",
8
+ "license": "MIT",
9
+ "scripts": {
10
+ "compile": "rm -rf dist/* && tsc -p tsconfig.pkg.json",
11
+ "compile-watch": "tsc -p tsconfig.pkg.json --watch",
12
+ "test": "vitest run",
13
+ "test-watch": "vitest watch",
14
+ "coverage": "vitest run --coverage"
15
+ },
16
+ "dependencies": {
17
+ "@isograph/disposable-types": "0.0.0-main-4ef7c123",
18
+ "react": "^18.2.0"
19
+ },
20
+ "devDependencies": {
21
+ "@types/react": "^18.0.31",
22
+ "react-test-renderer": "^18.2.0",
23
+ "typescript": "^5.0.3",
24
+ "vitest": "^0.29.8"
25
+ }
26
+ }
@@ -0,0 +1,202 @@
1
+ import { describe, test, vi, expect, assert } from "vitest";
2
+ import { createReferenceCountedPointer } from "./createReferenceCountedPointer";
3
+
4
+ describe("createReferenceCountedPointer", () => {
5
+ describe("it should not dispose the underlying item until all outstanding pointers are disposed", () => {
6
+ test("one pointer", () => {
7
+ const disposeItem = vi.fn();
8
+ const [pointer, disposePointer] = createReferenceCountedPointer([
9
+ 1,
10
+ disposeItem,
11
+ ]);
12
+ expect(disposeItem).not.toHaveBeenCalled();
13
+ disposePointer();
14
+ expect(disposeItem).toHaveBeenCalled();
15
+ });
16
+
17
+ test("linked list, FIFO", () => {
18
+ const disposeItem = vi.fn();
19
+ const [pointer, disposePointer] = createReferenceCountedPointer([
20
+ 1,
21
+ disposeItem,
22
+ ]);
23
+ const [pointer2, disposePointer2] = pointer.cloneIfNotDisposed()!;
24
+ const [pointer3, disposePointer3] = pointer2.cloneIfNotDisposed()!;
25
+ const [pointer4, disposePointer4] = pointer3.cloneIfNotDisposed()!;
26
+
27
+ disposePointer4();
28
+ disposePointer3();
29
+ disposePointer2();
30
+ expect(disposeItem).not.toHaveBeenCalled();
31
+ disposePointer();
32
+ expect(disposeItem).toHaveBeenCalled();
33
+ });
34
+
35
+ test("linked list, LIFO", () => {
36
+ const disposeItem = vi.fn();
37
+ const [pointer, disposePointer] = createReferenceCountedPointer([
38
+ 1,
39
+ disposeItem,
40
+ ]);
41
+ const [pointer2, disposePointer2] = pointer.cloneIfNotDisposed()!;
42
+ const [pointer3, disposePointer3] = pointer2.cloneIfNotDisposed()!;
43
+ const [pointer4, disposePointer4] = pointer3.cloneIfNotDisposed()!;
44
+
45
+ disposePointer();
46
+ disposePointer2();
47
+ disposePointer3();
48
+ expect(disposeItem).not.toHaveBeenCalled();
49
+ disposePointer4();
50
+ expect(disposeItem).toHaveBeenCalled();
51
+ });
52
+
53
+ test("linked list, mixed order", () => {
54
+ const disposeItem = vi.fn();
55
+ const [pointer, disposePointer] = createReferenceCountedPointer([
56
+ 1,
57
+ disposeItem,
58
+ ]);
59
+ const [pointer2, disposePointer2] = pointer.cloneIfNotDisposed()!;
60
+ const [pointer3, disposePointer3] = pointer2.cloneIfNotDisposed()!;
61
+ const [pointer4, disposePointer4] = pointer3.cloneIfNotDisposed()!;
62
+
63
+ disposePointer2();
64
+ disposePointer();
65
+ disposePointer4();
66
+ expect(disposeItem).not.toHaveBeenCalled();
67
+ disposePointer3();
68
+ expect(disposeItem).toHaveBeenCalled();
69
+ });
70
+
71
+ test("DAG, from root", () => {
72
+ const disposeItem = vi.fn();
73
+ const [pointer, disposePointer] = createReferenceCountedPointer([
74
+ 1,
75
+ disposeItem,
76
+ ]);
77
+ const [pointerA, disposePointerA] = pointer.cloneIfNotDisposed()!;
78
+ const [pointerA_1, disposePointerA_1] = pointerA.cloneIfNotDisposed()!;
79
+ const [pointerA_2, disposePointerA_2] = pointerA.cloneIfNotDisposed()!;
80
+ const [pointerB, disposePointerB] = pointer.cloneIfNotDisposed()!;
81
+ const [pointerB_1, disposePointerB_1] = pointerB.cloneIfNotDisposed()!;
82
+ const [pointerB_2, disposePointerB_2] = pointerB.cloneIfNotDisposed()!;
83
+
84
+ disposePointer();
85
+ disposePointerA();
86
+ disposePointerA_1();
87
+ disposePointerA_2();
88
+ disposePointerB();
89
+ disposePointerB_1();
90
+ expect(disposeItem).not.toHaveBeenCalled();
91
+ disposePointerB_2();
92
+ expect(disposeItem).toHaveBeenCalled();
93
+ });
94
+
95
+ test("DAG, from leaves", () => {
96
+ const disposeItem = vi.fn();
97
+ const [pointer, disposePointer] = createReferenceCountedPointer([
98
+ 1,
99
+ disposeItem,
100
+ ]);
101
+ const [pointerA, disposePointerA] = pointer.cloneIfNotDisposed()!;
102
+ const [pointerA_1, disposePointerA_1] = pointerA.cloneIfNotDisposed()!;
103
+ const [pointerA_2, disposePointerA_2] = pointerA.cloneIfNotDisposed()!;
104
+ const [pointerB, disposePointerB] = pointer.cloneIfNotDisposed()!;
105
+ const [pointerB_1, disposePointerB_1] = pointerB.cloneIfNotDisposed()!;
106
+ const [pointerB_2, disposePointerB_2] = pointerB.cloneIfNotDisposed()!;
107
+
108
+ disposePointerB_1();
109
+ disposePointerB_2();
110
+ disposePointerB();
111
+ disposePointerA_1();
112
+ disposePointerA_2();
113
+ disposePointerA();
114
+ expect(disposeItem).not.toHaveBeenCalled();
115
+ disposePointer();
116
+ expect(disposeItem).toHaveBeenCalled();
117
+ });
118
+
119
+ test("DAG, random", () => {
120
+ const disposeItem = vi.fn();
121
+ const [pointer, disposePointer] = createReferenceCountedPointer([
122
+ 1,
123
+ disposeItem,
124
+ ]);
125
+ const [pointerA, disposePointerA] = pointer.cloneIfNotDisposed()!;
126
+ const [pointerA_1, disposePointerA_1] = pointerA.cloneIfNotDisposed()!;
127
+ const [pointerA_2, disposePointerA_2] = pointerA.cloneIfNotDisposed()!;
128
+ const [pointerB, disposePointerB] = pointer.cloneIfNotDisposed()!;
129
+ const [pointerB_1, disposePointerB_1] = pointerB.cloneIfNotDisposed()!;
130
+ const [pointerB_2, disposePointerB_2] = pointerB.cloneIfNotDisposed()!;
131
+
132
+ disposePointerB_1();
133
+ disposePointerA();
134
+ disposePointer();
135
+ disposePointerB_2();
136
+ disposePointerB();
137
+ disposePointerA_2();
138
+ expect(disposeItem).not.toHaveBeenCalled();
139
+ disposePointerA_1();
140
+ expect(disposeItem).toHaveBeenCalled();
141
+ });
142
+ });
143
+
144
+ test("it should throw when disposed twice", () => {
145
+ const disposeItem = vi.fn();
146
+ const [pointer, disposePointer] = createReferenceCountedPointer([
147
+ 1,
148
+ disposeItem,
149
+ ]);
150
+ disposePointer();
151
+ expect(() => {
152
+ disposePointer();
153
+ }).toThrow();
154
+ });
155
+
156
+ test("it should return null when you attempt to retain a disposed pointer", () => {
157
+ const disposeItem = vi.fn();
158
+ const [pointer, disposePointer] = createReferenceCountedPointer([
159
+ 1,
160
+ disposeItem,
161
+ ]);
162
+ disposePointer();
163
+ expect(pointer.cloneIfNotDisposed()).toBe(null);
164
+ });
165
+
166
+ test("it should expose the underlying object only when undisposed", () => {
167
+ const disposeItem = vi.fn();
168
+ const [pointer, disposePointer] = createReferenceCountedPointer([
169
+ 1,
170
+ disposeItem,
171
+ ]);
172
+ expect(pointer.getItemIfNotDisposed()).toBe(1);
173
+ disposePointer();
174
+ expect(pointer.getItemIfNotDisposed()).toBe(null);
175
+ });
176
+
177
+ test("it should accurately report its disposed status", () => {
178
+ const disposeItem = vi.fn();
179
+ const [pointer, disposePointer] = createReferenceCountedPointer([
180
+ 1,
181
+ disposeItem,
182
+ ]);
183
+ expect(pointer.isDisposed()).toBe(false);
184
+ disposePointer();
185
+ expect(pointer.isDisposed()).toBe(true);
186
+ });
187
+
188
+ test("disposable status is unaffected by the presence of other undisposed pointers", () => {
189
+ const disposeItem = vi.fn();
190
+ const [pointer, disposePointer] = createReferenceCountedPointer([
191
+ 1,
192
+ disposeItem,
193
+ ]);
194
+ const pointer2 = pointer.cloneIfNotDisposed();
195
+ assert(pointer2 != null);
196
+ expect(pointer2[0].isDisposed()).toBe(false);
197
+ expect(pointer.isDisposed()).toBe(false);
198
+ disposePointer();
199
+ expect(pointer.isDisposed()).toBe(true);
200
+ expect(pointer2[0].isDisposed()).toBe(false);
201
+ });
202
+ });
@@ -0,0 +1,147 @@
1
+ import type { CleanupFn, ItemCleanupPair } from "@isograph/disposable-types";
2
+
3
+ // TODO cloneIfNotDisposed should also return the underlying item
4
+
5
+ /**
6
+ * Create an undisposed reference-counted pointer guarding a given item.
7
+ *
8
+ * Once all reference-counted pointers guarding a given item have been
9
+ * disposed, the underlying item will be disposed.
10
+ *
11
+ * Additional reference-counted pointers guarding the same item can be
12
+ * created by calling retainIfNotDisposed().
13
+ *
14
+ * ## Structural sharing
15
+ *
16
+ * Reference counted pointers enable reusing disposable items between
17
+ * application states, so-called structural sharing.
18
+ *
19
+ * If state 1 contains a reference counted pointer to an item, in order
20
+ * to transition to state 2, one would first create an additional
21
+ * reference-counted pointer by calling cloneIfNotDisposed, transition
22
+ * to state 2, then clean up state 1 by disposing of its reference-
23
+ * counted pointers. In this transition, at no time were there zero
24
+ * undisposed reference countend pointers to the disposable item, so it
25
+ * was never disposed, and we could reuse it between states.
26
+ */
27
+ export function createReferenceCountedPointer<T>(
28
+ pair: ItemCleanupPair<T>
29
+ ): ItemCleanupPair<ReferenceCountedPointer<T>> {
30
+ const originalReferenceCountedPointer = new RefCounter(pair);
31
+
32
+ return originalReferenceCountedPointer.retainIfNotDisposed()!;
33
+ }
34
+
35
+ export interface ReferenceCountedPointer<T> {
36
+ isDisposed(): boolean;
37
+ /**
38
+ * Safety: the item returned here is valid for use only as long as the reference
39
+ * counted pointer is not disposed.
40
+ */
41
+ getItemIfNotDisposed(): T | null;
42
+ cloneIfNotDisposed(): ItemCleanupPair<ReferenceCountedPointer<T>> | null;
43
+ }
44
+
45
+ type RefCountState<T> = {
46
+ item: T;
47
+ dispose: CleanupFn;
48
+ // Invariant: >0
49
+ activeReferenceCount: number;
50
+ };
51
+
52
+ // N.B. this could implement ReferenceCountedPointer<T>, but it would not be correct to use it
53
+ // as such, since it does not have an associated dispose function that can be called.
54
+ //
55
+ // Note that there is no way, and should be no way, to determine whether the underlying item
56
+ // has been disposed, let alone force it to be disposed! If you need that, you need to keep track
57
+ // of all calls to retainIfNotDisposed.
58
+ class RefCounter<T> {
59
+ private __state: RefCountState<T> | null;
60
+
61
+ /**
62
+ * Private. Do not expose this class directly, as this contructor creates a ReferenceCountedPointer
63
+ * in an invalid state. We must immediately, after creation, call retainIfNotDisposed().
64
+ */
65
+ constructor([item, dispose]: ItemCleanupPair<T>) {
66
+ this.__state = {
67
+ item,
68
+ dispose,
69
+ activeReferenceCount: 0,
70
+ };
71
+ }
72
+
73
+ getIfNotDisposed(): T | null {
74
+ return this.__state === null ? null : this.__state.item;
75
+ }
76
+
77
+ retainIfNotDisposed(): ItemCleanupPair<ReferenceCountedPointer<T>> | null {
78
+ if (this.__state !== null) {
79
+ this.__state.activeReferenceCount++;
80
+
81
+ const activeReference = new ActiveReference(this);
82
+
83
+ let disposed = false;
84
+ const dispose = () => {
85
+ if (disposed) {
86
+ throw new Error(
87
+ "Do not dispose an already-disposed ActiveReference."
88
+ );
89
+ }
90
+ disposed = true;
91
+ if (activeReference.__original === null) {
92
+ throw new Error(
93
+ "Attempted to dispose an active reference, but it was already disposed. " +
94
+ "This indicates a bug in reference-counted-pointer."
95
+ );
96
+ }
97
+ activeReference.__original = null;
98
+ if (this.__state === null) {
99
+ throw new Error(
100
+ "Attempted to dispose, but the underlying reference counted pointer was disposed. " +
101
+ "This indicates a bug in reference-counted-pointer."
102
+ );
103
+ }
104
+ this.__state.activeReferenceCount--;
105
+ this.__maybeDispose();
106
+ };
107
+
108
+ return [activeReference, dispose];
109
+ } else {
110
+ return null;
111
+ }
112
+ }
113
+
114
+ private __maybeDispose() {
115
+ if (this.__state === null) {
116
+ throw new Error(
117
+ "__maybeDispose was called, but the reference counted pointer was disposed. " +
118
+ "This indicates a bug in reference-counted-pointer."
119
+ );
120
+ }
121
+ if (this.__state.activeReferenceCount === 0) {
122
+ this.__state.dispose();
123
+ this.__state = null;
124
+ }
125
+ }
126
+ }
127
+
128
+ class ActiveReference<T> implements ReferenceCountedPointer<T> {
129
+ // Invariant: __original !== null => the original is not disposed.
130
+ __original: RefCounter<T> | null;
131
+
132
+ constructor(original: RefCounter<T>) {
133
+ this.__original = original;
134
+ }
135
+
136
+ isDisposed(): boolean {
137
+ return this.__original === null;
138
+ }
139
+
140
+ cloneIfNotDisposed(): ItemCleanupPair<ReferenceCountedPointer<T>> | null {
141
+ return this.__original?.retainIfNotDisposed() ?? null;
142
+ }
143
+
144
+ getItemIfNotDisposed(): T | null {
145
+ return this.__original?.getIfNotDisposed() ?? null;
146
+ }
147
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./createReferenceCountedPointer";
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.build.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist/",
5
+ "rootDir": "./src/",
6
+ "declaration": true
7
+ },
8
+ "include": ["./src/**/*.ts"]
9
+ }