@evolu/common 6.0.1-preview.2 → 6.0.1-preview.20
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/dist/src/Assert.d.ts +6 -3
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +7 -4
- package/dist/src/Brand.d.ts +75 -0
- package/dist/src/Brand.d.ts.map +1 -0
- package/dist/src/Brand.js +1 -0
- package/dist/src/Buffer.d.ts +1 -1
- package/dist/src/Buffer.d.ts.map +1 -1
- package/dist/src/Buffer.js +1 -1
- package/dist/src/CallbackRegistry.d.ts +53 -0
- package/dist/src/CallbackRegistry.d.ts.map +1 -0
- package/dist/src/CallbackRegistry.js +25 -0
- package/dist/src/Console.d.ts +31 -6
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +72 -9
- package/dist/src/Crypto.d.ts +61 -34
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +29 -42
- package/dist/src/Evolu/Db.d.ts +137 -66
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Db.js +300 -705
- package/dist/src/Evolu/Diff.d.ts +3 -3
- package/dist/src/Evolu/Diff.d.ts.map +1 -1
- package/dist/src/Evolu/Diff.js +7 -5
- package/dist/src/Evolu/Evolu.d.ts +200 -115
- package/dist/src/Evolu/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu/Evolu.js +287 -157
- package/dist/src/Evolu/Internal.d.ts +0 -2
- package/dist/src/Evolu/Internal.d.ts.map +1 -1
- package/dist/src/Evolu/Internal.js +0 -2
- package/dist/src/Evolu/LocalAuth.d.ts +144 -0
- package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
- package/dist/src/Evolu/LocalAuth.js +171 -0
- package/dist/src/Evolu/Owner.d.ts +152 -127
- package/dist/src/Evolu/Owner.d.ts.map +1 -1
- package/dist/src/Evolu/Owner.js +88 -107
- package/dist/src/Evolu/Platform.d.ts +9 -7
- package/dist/src/Evolu/Platform.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.d.ts +170 -186
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +487 -343
- package/dist/src/Evolu/Public.d.ts +6 -8
- package/dist/src/Evolu/Public.d.ts.map +1 -1
- package/dist/src/Evolu/Public.js +2 -3
- package/dist/src/Evolu/PublicKysely.js +3 -3
- package/dist/src/Evolu/Query.d.ts +2 -1
- package/dist/src/Evolu/Query.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.d.ts +4 -3
- package/dist/src/Evolu/Relay.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.js +50 -12
- package/dist/src/Evolu/Schema.d.ts +129 -73
- package/dist/src/Evolu/Schema.d.ts.map +1 -1
- package/dist/src/Evolu/Schema.js +169 -89
- package/dist/src/Evolu/Storage.d.ts +159 -14
- package/dist/src/Evolu/Storage.d.ts.map +1 -1
- package/dist/src/Evolu/Storage.js +42 -32
- package/dist/src/Evolu/Sync.d.ts +77 -13
- package/dist/src/Evolu/Sync.d.ts.map +1 -1
- package/dist/src/Evolu/Sync.js +453 -20
- package/dist/src/Evolu/Timestamp.d.ts +35 -27
- package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
- package/dist/src/Evolu/Timestamp.js +27 -18
- package/dist/src/ManyToManyMap.d.ts +74 -10
- package/dist/src/ManyToManyMap.d.ts.map +1 -1
- package/dist/src/ManyToManyMap.js +41 -6
- package/dist/src/Number.d.ts +2 -1
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Random.d.ts +3 -2
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/RefCountedResourceManager.d.ts +119 -0
- package/dist/src/RefCountedResourceManager.d.ts.map +1 -0
- package/dist/src/RefCountedResourceManager.js +197 -0
- package/dist/src/Result.d.ts +147 -51
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +7 -241
- package/dist/src/Sqlite.d.ts +21 -4
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +50 -8
- package/dist/src/Task.d.ts +511 -0
- package/dist/src/Task.d.ts.map +1 -0
- package/dist/src/Task.js +410 -0
- package/dist/src/Time.d.ts +59 -0
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +87 -4
- package/dist/src/Type.d.ts +462 -327
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +490 -436
- package/dist/src/Types.d.ts +0 -74
- package/dist/src/Types.d.ts.map +1 -1
- package/dist/src/WebSocket.d.ts +5 -2
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +12 -13
- package/dist/src/Worker.d.ts +39 -11
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +22 -4
- package/dist/src/index.d.ts +3 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -3
- package/package.json +14 -13
- package/src/Assert.ts +8 -7
- package/src/Brand.ts +75 -0
- package/src/Buffer.ts +1 -1
- package/src/CallbackRegistry.ts +84 -0
- package/src/Console.ts +91 -11
- package/src/Crypto.ts +94 -79
- package/src/Evolu/Db.ts +505 -1027
- package/src/Evolu/Diff.ts +7 -5
- package/src/Evolu/Evolu.ts +682 -329
- package/src/Evolu/Internal.ts +0 -2
- package/src/Evolu/LocalAuth.ts +422 -0
- package/src/Evolu/Owner.ts +221 -235
- package/src/Evolu/Platform.ts +9 -9
- package/src/Evolu/Protocol.ts +681 -614
- package/src/Evolu/Public.ts +7 -14
- package/src/Evolu/PublicKysely.ts +3 -3
- package/src/Evolu/Query.ts +2 -1
- package/src/Evolu/Relay.ts +62 -16
- package/src/Evolu/Schema.ts +391 -191
- package/src/Evolu/Storage.ts +275 -55
- package/src/Evolu/Sync.ts +758 -37
- package/src/Evolu/Timestamp.ts +38 -35
- package/src/ManyToManyMap.ts +127 -24
- package/src/Number.ts +2 -6
- package/src/Random.ts +3 -2
- package/src/RefCountedResourceManager.ts +368 -0
- package/src/Result.ts +152 -53
- package/src/Sqlite.ts +59 -23
- package/src/Task.ts +779 -0
- package/src/Time.ts +168 -4
- package/src/Type.ts +696 -660
- package/src/Types.ts +0 -76
- package/src/WebSocket.ts +23 -17
- package/src/Worker.ts +72 -23
- package/src/index.ts +3 -3
- package/dist/src/Callbacks.d.ts +0 -20
- package/dist/src/Callbacks.d.ts.map +0 -1
- package/dist/src/Callbacks.js +0 -18
- package/dist/src/Evolu/Config.d.ts +0 -69
- package/dist/src/Evolu/Config.d.ts.map +0 -1
- package/dist/src/Evolu/Config.js +0 -9
- package/dist/src/Evolu/Kysely.d.ts +0 -6
- package/dist/src/Evolu/Kysely.d.ts.map +0 -1
- package/dist/src/Evolu/Kysely.js +0 -21
- package/dist/src/NanoId.d.ts +0 -27
- package/dist/src/NanoId.d.ts.map +0 -1
- package/dist/src/NanoId.js +0 -6
- package/dist/src/Promise.d.ts +0 -180
- package/dist/src/Promise.d.ts.map +0 -1
- package/dist/src/Promise.js +0 -176
- package/src/Callbacks.ts +0 -43
- package/src/Evolu/Config.ts +0 -83
- package/src/Evolu/Kysely.ts +0 -38
- package/src/NanoId.ts +0 -39
- package/src/Promise.ts +0 -295
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { err, ok, Result } from "./Result.js";
|
|
2
|
+
import { PositiveInt } from "./Type.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A generic resource manager that handles reference counting and delayed
|
|
6
|
+
* disposal of shared resources. Useful for managing expensive resources like
|
|
7
|
+
* WebSocket connections, database connections, or file handles that need to be
|
|
8
|
+
* shared among multiple consumers.
|
|
9
|
+
*/
|
|
10
|
+
export interface RefCountedResourceManager<
|
|
11
|
+
TResource extends Disposable,
|
|
12
|
+
TResourceKey extends string,
|
|
13
|
+
TResourceConfig,
|
|
14
|
+
TConsumer,
|
|
15
|
+
TConsumerId extends string,
|
|
16
|
+
> extends Disposable {
|
|
17
|
+
/**
|
|
18
|
+
* Adds a consumer to resources, creating them if necessary. Increments
|
|
19
|
+
* reference counts for existing consumer-resource pairs.
|
|
20
|
+
*/
|
|
21
|
+
readonly addConsumer: (
|
|
22
|
+
consumer: TConsumer,
|
|
23
|
+
resourceConfigs: ReadonlyArray<TResourceConfig>,
|
|
24
|
+
) => void;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Removes a consumer from resources. Decrements reference counts and
|
|
28
|
+
* schedules disposal when no consumers remain.
|
|
29
|
+
*
|
|
30
|
+
* Returns an error if the resource doesn't exist or if the consumer wasn't
|
|
31
|
+
* added to the resource.
|
|
32
|
+
*/
|
|
33
|
+
readonly removeConsumer: (
|
|
34
|
+
consumer: TConsumer,
|
|
35
|
+
resourceConfigs: ReadonlyArray<TResourceConfig>,
|
|
36
|
+
) => Result<
|
|
37
|
+
void,
|
|
38
|
+
| ResourceNotFoundError<TResourceKey>
|
|
39
|
+
| ConsumerNotFoundError<TConsumerId, TResourceKey>
|
|
40
|
+
>;
|
|
41
|
+
|
|
42
|
+
/** Gets the resource for the specified key, or null if it doesn't exist. */
|
|
43
|
+
readonly getResource: (key: TResourceKey) => TResource | null;
|
|
44
|
+
|
|
45
|
+
/** Gets all consumer IDs currently using the specified resource key. */
|
|
46
|
+
readonly getConsumersForResource: (
|
|
47
|
+
key: TResourceKey,
|
|
48
|
+
) => ReadonlyArray<TConsumerId>;
|
|
49
|
+
|
|
50
|
+
/** Checks if a consumer is currently using any resources. */
|
|
51
|
+
readonly hasConsumerAnyResource: (consumer: TConsumer) => boolean;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Gets the consumer for the specified consumer ID, or null if not found or
|
|
55
|
+
* not using any resources.
|
|
56
|
+
*/
|
|
57
|
+
readonly getConsumer: (consumerId: TConsumerId) => TConsumer | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Error when trying to remove a consumer from a resource that doesn't exist. */
|
|
61
|
+
export interface ResourceNotFoundError<TResourceKey extends string = string> {
|
|
62
|
+
readonly type: "ResourceNotFoundError";
|
|
63
|
+
readonly resourceKey: TResourceKey;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Error when trying to remove a consumer that wasn't added to a resource. */
|
|
67
|
+
export interface ConsumerNotFoundError<
|
|
68
|
+
TConsumerId extends string = string,
|
|
69
|
+
TResourceKey extends string = string,
|
|
70
|
+
> {
|
|
71
|
+
readonly type: "ConsumerNotFoundError";
|
|
72
|
+
readonly consumerId: TConsumerId;
|
|
73
|
+
readonly resourceKey: TResourceKey;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface ResourceManagerConfig<
|
|
77
|
+
TResource extends Disposable,
|
|
78
|
+
TResourceKey extends string,
|
|
79
|
+
TResourceConfig,
|
|
80
|
+
TConsumer,
|
|
81
|
+
TConsumerId extends string,
|
|
82
|
+
> {
|
|
83
|
+
/** Creates a new resource for the given config. */
|
|
84
|
+
readonly createResource: (config: TResourceConfig) => TResource;
|
|
85
|
+
|
|
86
|
+
/** Extracts a unique key from a resource config for deduplication. */
|
|
87
|
+
readonly getResourceKey: (config: TResourceConfig) => TResourceKey;
|
|
88
|
+
|
|
89
|
+
/** Extracts a unique identifier from a consumer for reference counting. */
|
|
90
|
+
readonly getConsumerId: (consumer: TConsumer) => TConsumerId;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Delay in milliseconds before disposing unused resources. Helps avoid
|
|
94
|
+
* resource churn during rapid add/remove cycles. Defaults to 100ms.
|
|
95
|
+
*/
|
|
96
|
+
readonly disposalDelay?: number;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Called when a consumer is added to a resource for the first time. This
|
|
100
|
+
* happens when the consumer's reference count goes from 0 to 1 for this
|
|
101
|
+
* resource.
|
|
102
|
+
*/
|
|
103
|
+
readonly onConsumerAdded?: (
|
|
104
|
+
consumer: TConsumer,
|
|
105
|
+
resource: TResource,
|
|
106
|
+
resourceKey: TResourceKey,
|
|
107
|
+
) => void;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Called when a consumer is completely removed from a resource. This happens
|
|
111
|
+
* when the consumer's reference count goes from 1 to 0 for this resource.
|
|
112
|
+
*/
|
|
113
|
+
readonly onConsumerRemoved?: (
|
|
114
|
+
consumer: TConsumer,
|
|
115
|
+
resource: TResource,
|
|
116
|
+
resourceKey: TResourceKey,
|
|
117
|
+
) => void;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Creates a reference-counted resource manager.
|
|
122
|
+
*
|
|
123
|
+
* This manager tracks which consumers are using which resources and maintains
|
|
124
|
+
* reference counts to know when it's safe to dispose resources. Resources are
|
|
125
|
+
* created on-demand and disposed with a configurable delay to avoid churn.
|
|
126
|
+
*
|
|
127
|
+
* ### Example Usage
|
|
128
|
+
*
|
|
129
|
+
* ```ts
|
|
130
|
+
* // WebSocket connections manager
|
|
131
|
+
* interface WebSocketConfig {
|
|
132
|
+
* readonly url: WebSocketUrl;
|
|
133
|
+
* }
|
|
134
|
+
*
|
|
135
|
+
* type WebSocketUrl = string & Brand<"WebSocketUrl">;
|
|
136
|
+
* type UserId = string & Brand<"UserId">;
|
|
137
|
+
*
|
|
138
|
+
* const wsManager = createRefCountedResourceManager<
|
|
139
|
+
* WebSocket,
|
|
140
|
+
* WebSocketUrl,
|
|
141
|
+
* WebSocketConfig,
|
|
142
|
+
* User,
|
|
143
|
+
* UserId
|
|
144
|
+
* >({
|
|
145
|
+
* createResource: (config) => new WebSocket(config.url),
|
|
146
|
+
* getResourceKey: (config) => config.url,
|
|
147
|
+
* getConsumerId: (user) => user.id,
|
|
148
|
+
* disposalDelay: 1000,
|
|
149
|
+
* });
|
|
150
|
+
*
|
|
151
|
+
* // Add users to WebSocket connections
|
|
152
|
+
* wsManager.addConsumer(user1, [
|
|
153
|
+
* { url: "ws://server1.com" as WebSocketUrl },
|
|
154
|
+
* { url: "ws://server2.com" as WebSocketUrl },
|
|
155
|
+
* ]);
|
|
156
|
+
* wsManager.addConsumer(user2, [
|
|
157
|
+
* { url: "ws://server1.com" as WebSocketUrl },
|
|
158
|
+
* ]);
|
|
159
|
+
*
|
|
160
|
+
* // Remove users - server1 stays alive (user2 still using it)
|
|
161
|
+
* wsManager.removeConsumer(user1, [
|
|
162
|
+
* { url: "ws://server1.com" as WebSocketUrl },
|
|
163
|
+
* { url: "ws://server2.com" as WebSocketUrl },
|
|
164
|
+
* ]);
|
|
165
|
+
*
|
|
166
|
+
* // server2 gets disposed after delay, server1 stays alive
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
export const createRefCountedResourceManager = <
|
|
170
|
+
TResource extends Disposable,
|
|
171
|
+
TResourceKey extends string,
|
|
172
|
+
TResourceConfig,
|
|
173
|
+
TConsumer,
|
|
174
|
+
TConsumerId extends string,
|
|
175
|
+
>(
|
|
176
|
+
config: ResourceManagerConfig<
|
|
177
|
+
TResource,
|
|
178
|
+
TResourceKey,
|
|
179
|
+
TResourceConfig,
|
|
180
|
+
TConsumer,
|
|
181
|
+
TConsumerId
|
|
182
|
+
>,
|
|
183
|
+
): RefCountedResourceManager<
|
|
184
|
+
TResource,
|
|
185
|
+
TResourceKey,
|
|
186
|
+
TResourceConfig,
|
|
187
|
+
TConsumer,
|
|
188
|
+
TConsumerId
|
|
189
|
+
> => {
|
|
190
|
+
let isDisposed = false;
|
|
191
|
+
|
|
192
|
+
const resources = new Map<TResourceKey, TResource>();
|
|
193
|
+
const consumerCounts = new Map<TResourceKey, Map<TConsumerId, PositiveInt>>();
|
|
194
|
+
const consumers = new Map<TConsumerId, TConsumer>();
|
|
195
|
+
const disposalTimeouts = new Map<
|
|
196
|
+
TResourceKey,
|
|
197
|
+
ReturnType<typeof setTimeout>
|
|
198
|
+
>();
|
|
199
|
+
|
|
200
|
+
const disposalDelay = config.disposalDelay ?? 100;
|
|
201
|
+
|
|
202
|
+
const ensureResource = (resourceConfig: TResourceConfig) => {
|
|
203
|
+
const key = config.getResourceKey(resourceConfig);
|
|
204
|
+
const timeout = disposalTimeouts.get(key);
|
|
205
|
+
if (timeout) {
|
|
206
|
+
clearTimeout(timeout);
|
|
207
|
+
disposalTimeouts.delete(key);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!resources.has(key)) {
|
|
211
|
+
const resource = config.createResource(resourceConfig);
|
|
212
|
+
resources.set(key, resource);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const scheduleDisposal = (key: TResourceKey): void => {
|
|
217
|
+
const timeout = setTimeout(() => {
|
|
218
|
+
const resource = resources.get(key);
|
|
219
|
+
if (resource) {
|
|
220
|
+
resource[Symbol.dispose]();
|
|
221
|
+
resources.delete(key);
|
|
222
|
+
}
|
|
223
|
+
disposalTimeouts.delete(key);
|
|
224
|
+
}, disposalDelay);
|
|
225
|
+
|
|
226
|
+
disposalTimeouts.set(key, timeout);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const manager: RefCountedResourceManager<
|
|
230
|
+
TResource,
|
|
231
|
+
TResourceKey,
|
|
232
|
+
TResourceConfig,
|
|
233
|
+
TConsumer,
|
|
234
|
+
TConsumerId
|
|
235
|
+
> = {
|
|
236
|
+
addConsumer: (consumer, resourceConfigs) => {
|
|
237
|
+
if (isDisposed) return;
|
|
238
|
+
|
|
239
|
+
const consumerId = config.getConsumerId(consumer);
|
|
240
|
+
|
|
241
|
+
// Store consumer (last added consumer for this ID)
|
|
242
|
+
consumers.set(consumerId, consumer);
|
|
243
|
+
|
|
244
|
+
for (const resourceConfig of resourceConfigs) {
|
|
245
|
+
ensureResource(resourceConfig);
|
|
246
|
+
const resourceKey = config.getResourceKey(resourceConfig);
|
|
247
|
+
|
|
248
|
+
let counts = consumerCounts.get(resourceKey);
|
|
249
|
+
if (!counts) {
|
|
250
|
+
counts = new Map<TConsumerId, PositiveInt>();
|
|
251
|
+
consumerCounts.set(resourceKey, counts);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const currentCount = counts.get(consumerId) ?? 0;
|
|
255
|
+
const newCount = currentCount + 1;
|
|
256
|
+
counts.set(consumerId, PositiveInt.orThrow(newCount));
|
|
257
|
+
|
|
258
|
+
// Call onConsumerAdded callback only when consumer is added for the first time (0 -> 1)
|
|
259
|
+
if (currentCount === 0 && config.onConsumerAdded) {
|
|
260
|
+
const resource = resources.get(resourceKey);
|
|
261
|
+
if (resource) {
|
|
262
|
+
config.onConsumerAdded(consumer, resource, resourceKey);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
removeConsumer: (consumer, resourceConfigs) => {
|
|
269
|
+
if (isDisposed) return ok();
|
|
270
|
+
|
|
271
|
+
const consumerId = config.getConsumerId(consumer);
|
|
272
|
+
|
|
273
|
+
for (const resourceConfig of resourceConfigs) {
|
|
274
|
+
const key = config.getResourceKey(resourceConfig);
|
|
275
|
+
const counts = consumerCounts.get(key);
|
|
276
|
+
if (!counts) {
|
|
277
|
+
return err({ type: "ResourceNotFoundError", resourceKey: key });
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const currentCount = counts.get(consumerId);
|
|
281
|
+
if (currentCount == null) {
|
|
282
|
+
return err({
|
|
283
|
+
type: "ConsumerNotFoundError",
|
|
284
|
+
consumerId: consumerId,
|
|
285
|
+
resourceKey: key,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (currentCount === 1) {
|
|
290
|
+
counts.delete(consumerId);
|
|
291
|
+
|
|
292
|
+
// Call onConsumerRemoved callback only when consumer is completely removed (1 -> 0)
|
|
293
|
+
if (config.onConsumerRemoved) {
|
|
294
|
+
const resource = resources.get(key);
|
|
295
|
+
if (resource) {
|
|
296
|
+
config.onConsumerRemoved(consumer, resource, key);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (counts.size === 0) {
|
|
301
|
+
consumerCounts.delete(key);
|
|
302
|
+
scheduleDisposal(key);
|
|
303
|
+
}
|
|
304
|
+
} else {
|
|
305
|
+
counts.set(consumerId, PositiveInt.orThrow(currentCount - 1));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (!manager.hasConsumerAnyResource(consumer)) {
|
|
310
|
+
consumers.delete(consumerId);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return ok();
|
|
314
|
+
},
|
|
315
|
+
|
|
316
|
+
getResource: (key) => {
|
|
317
|
+
if (isDisposed) return null;
|
|
318
|
+
return resources.get(key) ?? null;
|
|
319
|
+
},
|
|
320
|
+
|
|
321
|
+
getConsumersForResource: (key) => {
|
|
322
|
+
if (isDisposed) return [];
|
|
323
|
+
const counts = consumerCounts.get(key);
|
|
324
|
+
return counts ? Array.from(counts.keys()) : [];
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
hasConsumerAnyResource: (consumer) => {
|
|
328
|
+
if (isDisposed) return false;
|
|
329
|
+
const consumerId = config.getConsumerId(consumer);
|
|
330
|
+
// If slow, can be optimized with reverse index
|
|
331
|
+
return Array.from(consumerCounts.values()).some((counts) =>
|
|
332
|
+
counts.has(consumerId),
|
|
333
|
+
);
|
|
334
|
+
},
|
|
335
|
+
|
|
336
|
+
getConsumer: (consumerId) => {
|
|
337
|
+
if (isDisposed) return null;
|
|
338
|
+
const consumer = consumers.get(consumerId);
|
|
339
|
+
if (!consumer) return null;
|
|
340
|
+
|
|
341
|
+
// Only return consumer if it's currently using any resources
|
|
342
|
+
if (!manager.hasConsumerAnyResource(consumer)) {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return consumer;
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
[Symbol.dispose]: () => {
|
|
350
|
+
if (isDisposed) return;
|
|
351
|
+
isDisposed = true;
|
|
352
|
+
|
|
353
|
+
for (const timeout of disposalTimeouts.values()) {
|
|
354
|
+
clearTimeout(timeout);
|
|
355
|
+
}
|
|
356
|
+
disposalTimeouts.clear();
|
|
357
|
+
|
|
358
|
+
for (const resource of resources.values()) {
|
|
359
|
+
resource[Symbol.dispose]();
|
|
360
|
+
}
|
|
361
|
+
resources.clear();
|
|
362
|
+
consumerCounts.clear();
|
|
363
|
+
consumers.clear();
|
|
364
|
+
},
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
return manager;
|
|
368
|
+
};
|
package/src/Result.ts
CHANGED
|
@@ -1,36 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 🛡️ Type-safe errors
|
|
3
3
|
*
|
|
4
|
-
* ## Intro
|
|
5
|
-
*
|
|
6
4
|
* The problem with throwing an exception in JavaScript is that the caught error
|
|
7
5
|
* is always of an unknown type. The unknown type is a problem because we can't
|
|
8
6
|
* be sure all errors have been handled because the TypeScript compiler can't
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* Some other languages like Rust 🦀 or Haskell 📚 use a type-safe approach to
|
|
12
|
-
* error handling, where errors are explicitly represented as part of the return
|
|
13
|
-
* type, such as Result or Either, allowing the developer to handle all errors
|
|
14
|
-
* safely. ✅
|
|
7
|
+
* tell us.
|
|
15
8
|
*
|
|
16
|
-
*
|
|
9
|
+
* Languages like Rust 🦀 or Haskell 📚 use a type-safe approach to error
|
|
10
|
+
* handling, where errors are explicitly represented as part of the return type,
|
|
11
|
+
* such as Result or Either, allowing the developer to handle errors safely.
|
|
12
|
+
* TypeScript can have this too via the `Result` type.
|
|
17
13
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* interface Ok<T> {
|
|
22
|
-
* readonly ok: true;
|
|
23
|
-
* readonly value: T;
|
|
24
|
-
* }
|
|
25
|
-
*
|
|
26
|
-
* interface Err<E> {
|
|
27
|
-
* readonly ok: false;
|
|
28
|
-
* readonly error: E;
|
|
29
|
-
* }
|
|
30
|
-
*
|
|
31
|
-
* const ok = <T>(value: T): Ok<T> => ({ ok: true, value });
|
|
32
|
-
* const err = <E>(error: E): Err<E> => ({ ok: false, error });
|
|
33
|
-
* ```
|
|
14
|
+
* The `Result` type can be either {@link Ok} (success) or {@link Err} (error).
|
|
15
|
+
* Use {@link ok} to create a successful result and {@link err} to create an error
|
|
16
|
+
* result.
|
|
34
17
|
*
|
|
35
18
|
* Now let's look at how `Result` can be used for safe JSON parsing:
|
|
36
19
|
*
|
|
@@ -77,12 +60,10 @@
|
|
|
77
60
|
*
|
|
78
61
|
* Let's summarize it:
|
|
79
62
|
*
|
|
80
|
-
* - For
|
|
63
|
+
* - For safe code, use `ok` and `err`.
|
|
81
64
|
* - For unsafe code, use `trySync` or `tryAsync`.
|
|
82
|
-
* - For asynchronous safe code, use `Promise` with {@link Result}.
|
|
83
65
|
*
|
|
84
|
-
* Asynchronous safe (because of a Promise using Result) code
|
|
85
|
-
* straightforward:
|
|
66
|
+
* Asynchronous safe (because of a Promise using Result) code:
|
|
86
67
|
*
|
|
87
68
|
* ```ts
|
|
88
69
|
* const fetchUser = async (
|
|
@@ -103,9 +84,33 @@
|
|
|
103
84
|
* };
|
|
104
85
|
* ```
|
|
105
86
|
*
|
|
106
|
-
*
|
|
87
|
+
* ### Naming Convention
|
|
88
|
+
*
|
|
89
|
+
* - For values: `const user = getUser()`
|
|
90
|
+
* - For void operations: `const result = foo()` (unless it would clash)
|
|
91
|
+
* - For clashes, suffix the name: `const saveResult = save()`
|
|
92
|
+
*
|
|
93
|
+
* ```ts
|
|
94
|
+
* const processUser = () => {
|
|
95
|
+
* // we have a value
|
|
96
|
+
* const user = getUser();
|
|
97
|
+
* if (!user.ok) return user;
|
|
98
|
+
*
|
|
99
|
+
* // void operation
|
|
100
|
+
* const result = saveToDatabase(user.value);
|
|
101
|
+
* if (!result.ok) return result;
|
|
102
|
+
*
|
|
103
|
+
* // avoiding clash
|
|
104
|
+
* const deleteFromCacheResult = deleteFromCache();
|
|
105
|
+
* if (!deleteFromCacheResult.ok) return deleteFromCacheResult;
|
|
106
|
+
*
|
|
107
|
+
* return ok();
|
|
108
|
+
* };
|
|
109
|
+
* ```
|
|
110
|
+
*
|
|
111
|
+
* ### Examples
|
|
107
112
|
*
|
|
108
|
-
*
|
|
113
|
+
* #### Sequential Operations with Short-Circuiting
|
|
109
114
|
*
|
|
110
115
|
* When performing a sequence of operations where any failure should stop
|
|
111
116
|
* further processing, use the `Result` type with early returns.
|
|
@@ -155,30 +160,114 @@
|
|
|
155
160
|
* This approach ensures type-safe error handling, avoids nested try/catch
|
|
156
161
|
* blocks, and clearly communicates the control flow.
|
|
157
162
|
*
|
|
158
|
-
*
|
|
163
|
+
* #### A function with two different errors:
|
|
159
164
|
*
|
|
160
165
|
* ```ts
|
|
161
166
|
* const example = (value: string): Result<number, FooError | BarError> => {
|
|
162
167
|
* const foo = getFoo(value);
|
|
163
168
|
* if (!foo.ok) return foo;
|
|
164
169
|
*
|
|
165
|
-
* const bar =
|
|
170
|
+
* const bar = getBar(foo.value);
|
|
166
171
|
* if (!bar.ok) return bar;
|
|
167
172
|
*
|
|
168
173
|
* return ok(barToNumber(bar.value));
|
|
169
174
|
* };
|
|
170
175
|
* ```
|
|
171
176
|
*
|
|
172
|
-
*
|
|
177
|
+
* ### Handling Unexpected Errors
|
|
178
|
+
*
|
|
179
|
+
* Even with disciplined use of `trySync` and `tryAsync`, unexpected errors can
|
|
180
|
+
* still occur due to programming mistakes, third-party library bugs, or edge
|
|
181
|
+
* cases. These should be logged for debugging, but **unexpected errors are not
|
|
182
|
+
* recoverable** - they represent bugs that must be fixed.
|
|
183
|
+
*
|
|
184
|
+
* **Important**: "Graceful shutdown" and error recovery can only come from
|
|
185
|
+
* expected errors handled via the `Result` type. Unexpected errors should fail
|
|
186
|
+
* fast - the operation fails immediately and the error bubbles up.
|
|
187
|
+
*
|
|
188
|
+
* #### In Browser Environments
|
|
189
|
+
*
|
|
190
|
+
* ```ts
|
|
191
|
+
* // Global error handler for unexpected errors
|
|
192
|
+
* window.addEventListener("error", (event) => {
|
|
193
|
+
* console.error("Uncaught error:", event.error);
|
|
194
|
+
* // Send to error reporting service
|
|
195
|
+
* errorReportingService.report(event.error);
|
|
196
|
+
* });
|
|
197
|
+
*
|
|
198
|
+
* // For unhandled promise rejections
|
|
199
|
+
* window.addEventListener("unhandledrejection", (event) => {
|
|
200
|
+
* console.error("Unhandled promise rejection:", event.reason);
|
|
201
|
+
* errorReportingService.report(event.reason);
|
|
202
|
+
* });
|
|
203
|
+
* ```
|
|
204
|
+
*
|
|
205
|
+
* #### In Node.js Environments
|
|
206
|
+
*
|
|
207
|
+
* ```ts
|
|
208
|
+
* // Handle uncaught exceptions - log and fail fast
|
|
209
|
+
* process.on("uncaughtException", (error) => {
|
|
210
|
+
* console.error("Uncaught exception:", error);
|
|
211
|
+
* errorReportingService.report(error);
|
|
212
|
+
* // Exit immediately - unexpected errors are not recoverable
|
|
213
|
+
* process.exit(1);
|
|
214
|
+
* });
|
|
215
|
+
*
|
|
216
|
+
* // Handle unhandled promise rejections
|
|
217
|
+
* process.on("unhandledRejection", (reason) => {
|
|
218
|
+
* console.error("Unhandled promise rejection:", reason);
|
|
219
|
+
* errorReportingService.report(reason);
|
|
220
|
+
* });
|
|
221
|
+
* ```
|
|
222
|
+
*
|
|
223
|
+
* These global handlers serve as a safety net to log and report unexpected
|
|
224
|
+
* errors for debugging purposes. They do not attempt recovery - unexpected
|
|
225
|
+
* errors represent bugs that must be fixed. The discipline of explicit error
|
|
226
|
+
* handling through the `Result` pattern remains the primary approach for all
|
|
227
|
+
* recoverable scenarios.
|
|
228
|
+
*
|
|
229
|
+
* ### FAQ
|
|
173
230
|
*
|
|
174
|
-
*
|
|
231
|
+
* #### What if my function doesn't return a value on success?
|
|
175
232
|
*
|
|
176
233
|
* If your function performs an operation but doesn't need to return a value on
|
|
177
234
|
* success, you can use `Result<void, E>`. Using `Result<void, E>` is clearer
|
|
178
235
|
* than using `Result<true, E>` or `Result<null, E>` because it communicates
|
|
179
236
|
* that the function doesn't produce a value but can produce errors.
|
|
180
237
|
*
|
|
181
|
-
*
|
|
238
|
+
* #### When can a function return `void` instead of `Result<void, E>`?
|
|
239
|
+
*
|
|
240
|
+
* A function can safely return `void` (instead of `Result<void, E>`) when all
|
|
241
|
+
* unsafe code within it is properly wrapped with `trySync` or `tryAsync`. If
|
|
242
|
+
* developers consistently wrap all potentially throwing operations, then any
|
|
243
|
+
* function returning `void` is guaranteed not to throw and can be called
|
|
244
|
+
* without error handling.
|
|
245
|
+
*
|
|
246
|
+
* ```ts
|
|
247
|
+
* // ✅ Safe to return void - all unsafe code is wrapped
|
|
248
|
+
* const processData = (data: string): void => {
|
|
249
|
+
* const parseResult = trySync(
|
|
250
|
+
* () => JSON.parse(data),
|
|
251
|
+
* (error) => ({ type: "ParseError", message: String(error) }),
|
|
252
|
+
* );
|
|
253
|
+
*
|
|
254
|
+
* if (!parseResult.ok) {
|
|
255
|
+
* logError(parseResult.error); // Handle error appropriately
|
|
256
|
+
* return;
|
|
257
|
+
* }
|
|
258
|
+
*
|
|
259
|
+
* // Continue with safe operations...
|
|
260
|
+
* };
|
|
261
|
+
*
|
|
262
|
+
* // ✅ Can call without try-catch since it returns void
|
|
263
|
+
* processData(jsonString);
|
|
264
|
+
* ```
|
|
265
|
+
*
|
|
266
|
+
* This approach creates a clear contract: functions returning `void` are safe
|
|
267
|
+
* to call, while functions returning `Result<T, E>` require explicit error
|
|
268
|
+
* handling.
|
|
269
|
+
*
|
|
270
|
+
* #### How do I short-circuit processing of an array on the first error?
|
|
182
271
|
*
|
|
183
272
|
* If you want to stop processing as soon as an error occurs (short-circuit),
|
|
184
273
|
* you should produce and check each `Result` inside a loop:
|
|
@@ -194,12 +283,11 @@
|
|
|
194
283
|
* // All queries succeeded
|
|
195
284
|
* ```
|
|
196
285
|
*
|
|
197
|
-
*
|
|
286
|
+
* #### How do I handle an array of operations and short-circuit on the first error?
|
|
198
287
|
*
|
|
199
288
|
* If you have an array of operations (not results), you should make them
|
|
200
|
-
* _lazy_—that is, represent each operation as a function
|
|
201
|
-
*
|
|
202
|
-
* stop on the first error:
|
|
289
|
+
* _lazy_—that is, represent each operation as a function. This way, you only
|
|
290
|
+
* execute each operation as needed, and can stop on the first error:
|
|
203
291
|
*
|
|
204
292
|
* ```ts
|
|
205
293
|
* import type { LazyValue } from "./Function";
|
|
@@ -232,17 +320,7 @@
|
|
|
232
320
|
* above) over monadic helpers. Imperative code is generally more readable,
|
|
233
321
|
* easier to debug, and more familiar to most JavaScript and TypeScript
|
|
234
322
|
* developers. While monads and functional helpers can be powerful, they often
|
|
235
|
-
* obscure control flow and make debugging harder.
|
|
236
|
-
* handling explicit and straightforward.
|
|
237
|
-
*
|
|
238
|
-
* @module
|
|
239
|
-
*/
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* A `Result` can be either {@link Ok} (success) or {@link Err} (error).
|
|
243
|
-
*
|
|
244
|
-
* Use {@link ok} to create a successful result and {@link err} to create an error
|
|
245
|
-
* result.
|
|
323
|
+
* obscure control flow and make debugging harder.
|
|
246
324
|
*/
|
|
247
325
|
export type Result<T, E> = Ok<T> | Err<E>;
|
|
248
326
|
|
|
@@ -301,6 +379,22 @@ export interface Err<E> {
|
|
|
301
379
|
readonly error: E;
|
|
302
380
|
}
|
|
303
381
|
|
|
382
|
+
/**
|
|
383
|
+
* Extracts the value type from a {@link Result}.
|
|
384
|
+
*
|
|
385
|
+
* @category Utilities
|
|
386
|
+
*/
|
|
387
|
+
export type InferOk<R extends Result<any, any>> =
|
|
388
|
+
R extends Ok<infer T> ? T : never;
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Extracts the error type from a {@link Result}.
|
|
392
|
+
*
|
|
393
|
+
* @category Utilities
|
|
394
|
+
*/
|
|
395
|
+
export type InferErr<R extends Result<any, any>> =
|
|
396
|
+
R extends Err<infer E> ? E : never;
|
|
397
|
+
|
|
304
398
|
/**
|
|
305
399
|
* Creates an {@link Ok} result.
|
|
306
400
|
*
|
|
@@ -355,12 +449,14 @@ export const err = <E>(error: E): Err<E> => ({ ok: false, error });
|
|
|
355
449
|
* const config = getOrThrow(loadConfig());
|
|
356
450
|
* // Safe to use config here
|
|
357
451
|
* ```
|
|
452
|
+
*
|
|
453
|
+
* Throws: `Error` with the original error attached as `cause`.
|
|
358
454
|
*/
|
|
359
455
|
export const getOrThrow = <T, E>(result: Result<T, E>): T => {
|
|
360
456
|
if (result.ok) {
|
|
361
457
|
return result.value;
|
|
362
458
|
} else {
|
|
363
|
-
throw new Error(
|
|
459
|
+
throw new Error("getOrThrow", { cause: result.error });
|
|
364
460
|
}
|
|
365
461
|
};
|
|
366
462
|
|
|
@@ -384,7 +480,10 @@ export const getOrThrow = <T, E>(result: Result<T, E>): T => {
|
|
|
384
480
|
* const parseJson = (value: string): Result<unknown, ParseJsonError> =>
|
|
385
481
|
* trySync(
|
|
386
482
|
* () => JSON.parse(value) as unknown,
|
|
387
|
-
* (error)
|
|
483
|
+
* (error): ParseJsonError => ({
|
|
484
|
+
* type: "ParseJsonError",
|
|
485
|
+
* message: String(error),
|
|
486
|
+
* }),
|
|
388
487
|
* );
|
|
389
488
|
* ```
|
|
390
489
|
*/
|