@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 +7 -0
- package/dist/createReferenceCountedPointer.d.ts +33 -0
- package/dist/createReferenceCountedPointer.js +107 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +17 -0
- package/package.json +26 -0
- package/src/createReferenceCountedPointer.test.ts +202 -0
- package/src/createReferenceCountedPointer.ts +147 -0
- package/src/index.ts +1 -0
- package/tsconfig.pkg.json +9 -0
package/README.md
ADDED
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED
@@ -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";
|