@dxos/context 0.4.10-main.fa5a270 → 0.4.10-main.fd8ea31

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.
@@ -1,8 +1,9 @@
1
1
  // packages/common/context/src/context.ts
2
+ import { inspect } from "@dxos/node-std/util";
2
3
  import { log } from "@dxos/log";
3
4
  import { safeInstanceof } from "@dxos/util";
4
5
 
5
- // packages/common/context/src/context-disposed.ts
6
+ // packages/common/context/src/context-disposed-error.ts
6
7
  var ContextDisposedError = class extends Error {
7
8
  constructor() {
8
9
  super("Context disposed.");
@@ -22,6 +23,7 @@ function _ts_decorate(decorators, target, key, desc) {
22
23
  }
23
24
  var __dxlog_file = "/home/runner/work/dxos/dxos/packages/common/context/src/context.ts";
24
25
  var MAX_SAFE_DISPOSE_CALLBACKS = 300;
26
+ var _a, _b;
25
27
  var Context = class _Context {
26
28
  constructor({ onError = (error) => {
27
29
  if (error instanceof ContextDisposedError) {
@@ -35,12 +37,20 @@ var Context = class _Context {
35
37
  this._disposePromise = void 0;
36
38
  this._parent = null;
37
39
  this.maxSafeDisposeCallbacks = MAX_SAFE_DISPOSE_CALLBACKS;
40
+ this[_a] = "Context";
41
+ this[_b] = () => this.toString();
38
42
  this._onError = onError;
39
43
  this._attributes = attributes;
40
44
  if (parent !== void 0) {
41
45
  this._parent = parent;
42
46
  }
43
47
  }
48
+ static {
49
+ _a = Symbol.toStringTag, _b = inspect.custom;
50
+ }
51
+ static default() {
52
+ return new _Context();
53
+ }
44
54
  get disposed() {
45
55
  return this._isDisposed;
46
56
  }
@@ -64,7 +74,7 @@ var Context = class _Context {
64
74
  } catch (error) {
65
75
  log.catch(error, void 0, {
66
76
  F: __dxlog_file,
67
- L: 82,
77
+ L: 88,
68
78
  S: this,
69
79
  C: (f, a) => f(...a)
70
80
  });
@@ -78,7 +88,7 @@ var Context = class _Context {
78
88
  safeThreshold: this.maxSafeDisposeCallbacks
79
89
  }, {
80
90
  F: __dxlog_file,
81
- L: 89,
91
+ L: 95,
82
92
  S: this,
83
93
  C: (f, a) => f(...a)
84
94
  });
@@ -92,13 +102,12 @@ var Context = class _Context {
92
102
  }
93
103
  /**
94
104
  * Runs all dispose callbacks.
95
- * Sync callbacks are run in the reverse order they were added.
96
- * Async callbacks are run in parallel.
105
+ * Callbacks are run in the reverse order they were added.
97
106
  * This function never throws.
98
107
  * It is safe to ignore the returned promise if the caller does not wish to wait for callbacks to complete.
99
108
  * Disposing context means that onDispose will throw an error and any errors raised will be logged and not propagated.
100
109
  */
101
- dispose() {
110
+ async dispose() {
102
111
  if (this._disposePromise) {
103
112
  return this._disposePromise;
104
113
  }
@@ -107,27 +116,21 @@ var Context = class _Context {
107
116
  this._disposePromise = new Promise((resolve) => {
108
117
  resolveDispose = resolve;
109
118
  });
110
- const promises = [];
111
119
  const callbacks = Array.from(this._disposeCallbacks).reverse();
120
+ this._disposeCallbacks.length = 0;
112
121
  for (const callback of callbacks) {
113
- promises.push((async () => {
114
- try {
115
- await callback();
116
- } catch (error) {
117
- log.catch(error, void 0, {
118
- F: __dxlog_file,
119
- L: 132,
120
- S: this,
121
- C: (f, a) => f(...a)
122
- });
123
- }
124
- })());
122
+ try {
123
+ await callback();
124
+ } catch (error) {
125
+ log.catch(error, void 0, {
126
+ F: __dxlog_file,
127
+ L: 136,
128
+ S: this,
129
+ C: (f, a) => f(...a)
130
+ });
131
+ }
125
132
  }
126
- this._disposeCallbacks.length = 0;
127
- void Promise.all(promises).then(() => {
128
- resolveDispose();
129
- });
130
- return this._disposePromise;
133
+ resolveDispose();
131
134
  }
132
135
  /**
133
136
  * Raise the error inside the context.
@@ -173,6 +176,9 @@ var Context = class _Context {
173
176
  }
174
177
  return void 0;
175
178
  }
179
+ toString() {
180
+ return `Context(${this._isDisposed ? "disposed" : "active"})`;
181
+ }
176
182
  };
177
183
  Context = _ts_decorate([
178
184
  safeInstanceof("Context")
@@ -191,10 +197,127 @@ var cancelWithContext = (ctx, promise) => {
191
197
  })
192
198
  ]).finally(() => clearDispose?.());
193
199
  };
200
+
201
+ // packages/common/context/src/resource.ts
202
+ import "@dxos/util";
203
+ var LifecycleState;
204
+ (function(LifecycleState2) {
205
+ LifecycleState2["CLOSED"] = "CLOSED";
206
+ LifecycleState2["OPEN"] = "OPEN";
207
+ LifecycleState2["ERROR"] = "ERROR";
208
+ })(LifecycleState || (LifecycleState = {}));
209
+ var Resource = class {
210
+ #lifecycleState = "CLOSED";
211
+ #openPromise = null;
212
+ #closePromise = null;
213
+ /**
214
+ * Managed internally by the resource.
215
+ * Recreated on close.
216
+ * Errors are propagated to the `_catch` method and the parent context.
217
+ */
218
+ #internalCtx = this.#createContext();
219
+ /**
220
+ * Context that is used to bubble up errors that are not handled by the resource.
221
+ * Provided in the open method.
222
+ */
223
+ #parentCtx = new Context();
224
+ get _lifecycleState() {
225
+ return this.#lifecycleState;
226
+ }
227
+ get _ctx() {
228
+ return this.#internalCtx;
229
+ }
230
+ /**
231
+ * To be overridden by subclasses.
232
+ */
233
+ async _open(ctx) {
234
+ }
235
+ /**
236
+ * To be overridden by subclasses.
237
+ */
238
+ async _close(ctx) {
239
+ }
240
+ /**
241
+ * Error handler for errors that are caught by the context.
242
+ * By default errors are bubbled up to the parent context which is passed to the open method.
243
+ */
244
+ async _catch(err) {
245
+ throw err;
246
+ }
247
+ /**
248
+ * Opens the resource.
249
+ * If the resource is already open, it does nothing.
250
+ * If the resource is in an error state, it throws an error.
251
+ * If the resource is closed, it waits for it to close and then opens it.
252
+ * @param ctx - Context to use for opening the resource. This context will receive errors that are not handled in `_catch`.
253
+ */
254
+ async open(ctx) {
255
+ switch (this.#lifecycleState) {
256
+ case "OPEN":
257
+ return this;
258
+ case "ERROR":
259
+ throw new Error(`Invalid state: ${this.#lifecycleState}`);
260
+ default:
261
+ }
262
+ await this.#closePromise;
263
+ await (this.#openPromise ??= this.#open(ctx));
264
+ return this;
265
+ }
266
+ /**
267
+ * Closes the resource.
268
+ * If the resource is already closed, it does nothing.
269
+ */
270
+ async close(ctx) {
271
+ if (this.#lifecycleState === "CLOSED") {
272
+ return this;
273
+ }
274
+ await this.#openPromise;
275
+ await (this.#closePromise ??= this.#close(ctx));
276
+ return this;
277
+ }
278
+ async [Symbol.asyncDispose]() {
279
+ await this.close();
280
+ }
281
+ async #open(ctx) {
282
+ this.#closePromise = null;
283
+ if (ctx) {
284
+ this.#parentCtx = ctx;
285
+ }
286
+ await this._open(this.#parentCtx);
287
+ this.#lifecycleState = "OPEN";
288
+ }
289
+ async #close(ctx = new Context()) {
290
+ this.#openPromise = null;
291
+ await this.#internalCtx.dispose();
292
+ await this._close(ctx);
293
+ this.#internalCtx = this.#createContext();
294
+ this.#lifecycleState = "CLOSED";
295
+ }
296
+ #createContext() {
297
+ return new Context({
298
+ onError: (error) => queueMicrotask(async () => {
299
+ try {
300
+ await this._catch(error);
301
+ } catch (err) {
302
+ this.#lifecycleState = "ERROR";
303
+ this.#parentCtx.raise(err);
304
+ }
305
+ })
306
+ });
307
+ }
308
+ };
309
+ var openInContext = async (ctx, resource) => {
310
+ await resource.open?.(ctx);
311
+ ctx.onDispose(() => resource.close?.());
312
+ return resource;
313
+ };
194
314
  export {
195
315
  Context,
196
316
  ContextDisposedError,
317
+ LifecycleState,
318
+ Resource,
197
319
  cancelWithContext,
320
+ openInContext,
198
321
  rejectOnDispose
199
322
  };
200
323
  //# sourceMappingURL=index.mjs.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../../../src/context.ts", "../../../src/context-disposed.ts", "../../../src/promise-utils.ts"],
4
- "sourcesContent": ["//\n// Copyright 2022 DXOS.org\n//\n\nimport { log } from '@dxos/log';\nimport { safeInstanceof } from '@dxos/util';\n\nimport { ContextDisposedError } from './context-disposed';\n\nexport type ContextErrorHandler = (error: Error) => void;\n\nexport type DisposeCallback = () => void | Promise<void>;\n\nexport type CreateContextParams = {\n onError?: ContextErrorHandler;\n attributes?: Record<string, any>;\n parent?: Context;\n};\n\n/**\n * Maximum number of dispose callbacks before we start logging warnings.\n */\nconst MAX_SAFE_DISPOSE_CALLBACKS = 300;\n\n@safeInstanceof('Context')\nexport class Context {\n private readonly _onError: ContextErrorHandler;\n private readonly _disposeCallbacks: DisposeCallback[] = [];\n private _isDisposed = false;\n private _disposePromise?: Promise<void> = undefined;\n private _parent: Context | null = null;\n\n private _attributes: Record<string, any>;\n\n public maxSafeDisposeCallbacks = MAX_SAFE_DISPOSE_CALLBACKS;\n\n constructor({\n onError = (error) => {\n if (error instanceof ContextDisposedError) {\n return;\n }\n\n void this.dispose();\n\n // Will generate an unhandled rejection.\n throw error;\n },\n attributes = {},\n parent,\n }: CreateContextParams = {}) {\n this._onError = onError;\n this._attributes = attributes;\n if (parent !== undefined) {\n this._parent = parent;\n }\n }\n\n get disposed() {\n return this._isDisposed;\n }\n\n get disposeCallbacksLength() {\n return this._disposeCallbacks.length;\n }\n\n /**\n * Schedules a callback to run when the context is disposed.\n * May be async, in this case the disposer might choose to wait for all resource to released.\n * Throwing an error inside the callback will result in the error being logged, but not re-thrown.\n *\n * NOTE: Will call the callback immediately if the context is already disposed.\n *\n * @returns A function that can be used to remove the callback from the dispose list.\n */\n onDispose(callback: DisposeCallback) {\n if (this._isDisposed) {\n // Call the callback immediately if the context is already disposed.\n void (async () => {\n try {\n await callback();\n } catch (error: any) {\n log.catch(error);\n }\n })();\n }\n\n this._disposeCallbacks.push(callback);\n if (this._disposeCallbacks.length > this.maxSafeDisposeCallbacks) {\n log.warn('Context has a large number of dispose callbacks. This might be a memory leak.', {\n count: this._disposeCallbacks.length,\n safeThreshold: this.maxSafeDisposeCallbacks,\n });\n }\n\n return () => {\n const index = this._disposeCallbacks.indexOf(callback);\n if (index !== -1) {\n this._disposeCallbacks.splice(index, 1);\n }\n };\n }\n\n /**\n * Runs all dispose callbacks.\n * Sync callbacks are run in the reverse order they were added.\n * Async callbacks are run in parallel.\n * This function never throws.\n * It is safe to ignore the returned promise if the caller does not wish to wait for callbacks to complete.\n * Disposing context means that onDispose will throw an error and any errors raised will be logged and not propagated.\n */\n dispose(): Promise<void> {\n if (this._disposePromise) {\n return this._disposePromise;\n }\n this._isDisposed = true;\n\n // Set the promise before running the callbacks.\n let resolveDispose: () => void;\n this._disposePromise = new Promise<void>((resolve) => {\n resolveDispose = resolve;\n });\n\n const promises = [];\n // Clone the array so that any mutations to the original array don't affect the dispose process.\n const callbacks = Array.from(this._disposeCallbacks).reverse();\n for (const callback of callbacks) {\n promises.push(\n (async () => {\n try {\n await callback();\n } catch (error: any) {\n log.catch(error);\n }\n })(),\n );\n }\n this._disposeCallbacks.length = 0;\n\n void Promise.all(promises).then(() => {\n resolveDispose();\n });\n\n return this._disposePromise;\n }\n\n /**\n * Raise the error inside the context.\n * The error will be propagated to the error handler.\n * IF the error handler is not set, the error will dispose the context and cause an unhandled rejection.\n */\n raise(error: Error): void {\n if (this._isDisposed) {\n // TODO(dmaretskyi): Don't log those.\n // log.warn('Error in disposed context', error);\n return;\n }\n\n try {\n this._onError(error);\n } catch (err) {\n // Generate an unhandled rejection and stop the error propagation.\n void Promise.reject(err);\n }\n }\n\n derive({ onError, attributes }: CreateContextParams = {}): Context {\n const newCtx = new Context({\n // TODO(dmaretskyi): Optimize to not require allocating a new closure for every context.\n onError: async (error) => {\n if (!onError) {\n this.raise(error);\n } else {\n try {\n await onError(error);\n } catch {\n this.raise(error);\n }\n }\n },\n attributes,\n });\n const clearDispose = this.onDispose(() => newCtx.dispose());\n newCtx.onDispose(clearDispose);\n return newCtx;\n }\n\n getAttribute(key: string): any {\n if (key in this._attributes) {\n return this._attributes[key];\n }\n if (this._parent !== null) {\n return this._parent.getAttribute(key);\n }\n return undefined;\n }\n}\n", "//\n// Copyright 2023 DXOS.org\n//\n\nexport class ContextDisposedError extends Error {\n constructor() {\n super('Context disposed.');\n }\n}\n", "//\n// Copyright 2023 DXOS.org\n//\n\nimport { type Context } from './context';\nimport { ContextDisposedError } from './context-disposed';\n\n/**\n * @returns A promise that rejects when the context is disposed.\n */\n// TODO(dmaretskyi): Memory leak.\nexport const rejectOnDispose = (ctx: Context, error = new ContextDisposedError()): Promise<never> =>\n new Promise((resolve, reject) => {\n ctx.onDispose(() => reject(error));\n });\n\n/**\n * Rejects the promise if the context is disposed.\n */\nexport const cancelWithContext = <T>(ctx: Context, promise: Promise<T>): Promise<T> => {\n let clearDispose: () => void;\n return Promise.race([\n promise,\n new Promise<never>((resolve, reject) => {\n // Will be called before .finally() handlers.\n clearDispose = ctx.onDispose(() => reject(new ContextDisposedError()));\n }),\n ]).finally(() => clearDispose?.());\n};\n"],
5
- "mappings": "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",
6
- "names": ["log", "safeInstanceof", "ContextDisposedError", "Error", "constructor", "MAX_SAFE_DISPOSE_CALLBACKS", "Context", "constructor", "onError", "error", "ContextDisposedError", "dispose", "attributes", "parent", "_disposeCallbacks", "_isDisposed", "_disposePromise", "undefined", "_parent", "maxSafeDisposeCallbacks", "_onError", "_attributes", "disposed", "disposeCallbacksLength", "length", "onDispose", "callback", "log", "catch", "push", "warn", "count", "safeThreshold", "index", "indexOf", "splice", "resolveDispose", "Promise", "resolve", "promises", "callbacks", "Array", "from", "reverse", "all", "then", "raise", "err", "reject", "derive", "newCtx", "clearDispose", "getAttribute", "key", "safeInstanceof", "rejectOnDispose", "ctx", "error", "ContextDisposedError", "Promise", "resolve", "reject", "onDispose", "cancelWithContext", "promise", "clearDispose", "race", "finally"]
3
+ "sources": ["../../../src/context.ts", "../../../src/context-disposed-error.ts", "../../../src/promise-utils.ts", "../../../src/resource.ts"],
4
+ "sourcesContent": ["//\n// Copyright 2022 DXOS.org\n//\n\nimport { inspect } from 'node:util';\n\nimport { log } from '@dxos/log';\nimport { safeInstanceof } from '@dxos/util';\n\nimport { ContextDisposedError } from './context-disposed-error';\n\nexport type ContextErrorHandler = (error: Error) => void;\n\nexport type DisposeCallback = () => any | Promise<any>;\n\nexport type CreateContextParams = {\n onError?: ContextErrorHandler;\n attributes?: Record<string, any>;\n parent?: Context;\n};\n\n/**\n * Maximum number of dispose callbacks before we start logging warnings.\n */\nconst MAX_SAFE_DISPOSE_CALLBACKS = 300;\n\n@safeInstanceof('Context')\nexport class Context {\n static default() {\n return new Context();\n }\n\n private readonly _onError: ContextErrorHandler;\n private readonly _disposeCallbacks: DisposeCallback[] = [];\n private _isDisposed = false;\n private _disposePromise?: Promise<void> = undefined;\n private _parent: Context | null = null;\n\n private _attributes: Record<string, any>;\n\n public maxSafeDisposeCallbacks = MAX_SAFE_DISPOSE_CALLBACKS;\n\n constructor({\n onError = (error) => {\n if (error instanceof ContextDisposedError) {\n return;\n }\n\n void this.dispose();\n\n // Will generate an unhandled rejection.\n throw error;\n },\n attributes = {},\n parent,\n }: CreateContextParams = {}) {\n this._onError = onError;\n this._attributes = attributes;\n if (parent !== undefined) {\n this._parent = parent;\n }\n }\n\n get disposed() {\n return this._isDisposed;\n }\n\n get disposeCallbacksLength() {\n return this._disposeCallbacks.length;\n }\n\n /**\n * Schedules a callback to run when the context is disposed.\n * May be async, in this case the disposer might choose to wait for all resource to released.\n * Throwing an error inside the callback will result in the error being logged, but not re-thrown.\n *\n * NOTE: Will call the callback immediately if the context is already disposed.\n *\n * @returns A function that can be used to remove the callback from the dispose list.\n */\n onDispose(callback: DisposeCallback) {\n if (this._isDisposed) {\n // Call the callback immediately if the context is already disposed.\n void (async () => {\n try {\n await callback();\n } catch (error: any) {\n log.catch(error);\n }\n })();\n }\n\n this._disposeCallbacks.push(callback);\n if (this._disposeCallbacks.length > this.maxSafeDisposeCallbacks) {\n log.warn('Context has a large number of dispose callbacks. This might be a memory leak.', {\n count: this._disposeCallbacks.length,\n safeThreshold: this.maxSafeDisposeCallbacks,\n });\n }\n\n return () => {\n const index = this._disposeCallbacks.indexOf(callback);\n if (index !== -1) {\n this._disposeCallbacks.splice(index, 1);\n }\n };\n }\n\n /**\n * Runs all dispose callbacks.\n * Callbacks are run in the reverse order they were added.\n * This function never throws.\n * It is safe to ignore the returned promise if the caller does not wish to wait for callbacks to complete.\n * Disposing context means that onDispose will throw an error and any errors raised will be logged and not propagated.\n */\n async dispose(): Promise<void> {\n if (this._disposePromise) {\n return this._disposePromise;\n }\n this._isDisposed = true;\n\n // Set the promise before running the callbacks.\n let resolveDispose!: () => void;\n this._disposePromise = new Promise<void>((resolve) => {\n resolveDispose = resolve;\n });\n\n // Clone the array so that any mutations to the original array don't affect the dispose process.\n const callbacks = Array.from(this._disposeCallbacks).reverse();\n this._disposeCallbacks.length = 0;\n\n for (const callback of callbacks) {\n try {\n await callback();\n } catch (error: any) {\n log.catch(error);\n }\n }\n resolveDispose();\n }\n\n /**\n * Raise the error inside the context.\n * The error will be propagated to the error handler.\n * IF the error handler is not set, the error will dispose the context and cause an unhandled rejection.\n */\n raise(error: Error): void {\n if (this._isDisposed) {\n // TODO(dmaretskyi): Don't log those.\n // log.warn('Error in disposed context', error);\n return;\n }\n\n try {\n this._onError(error);\n } catch (err) {\n // Generate an unhandled rejection and stop the error propagation.\n void Promise.reject(err);\n }\n }\n\n derive({ onError, attributes }: CreateContextParams = {}): Context {\n const newCtx = new Context({\n // TODO(dmaretskyi): Optimize to not require allocating a new closure for every context.\n onError: async (error) => {\n if (!onError) {\n this.raise(error);\n } else {\n try {\n await onError(error);\n } catch {\n this.raise(error);\n }\n }\n },\n attributes,\n });\n const clearDispose = this.onDispose(() => newCtx.dispose());\n newCtx.onDispose(clearDispose);\n return newCtx;\n }\n\n getAttribute(key: string): any {\n if (key in this._attributes) {\n return this._attributes[key];\n }\n if (this._parent !== null) {\n return this._parent.getAttribute(key);\n }\n return undefined;\n }\n\n [Symbol.toStringTag] = 'Context';\n\n [inspect.custom] = () => this.toString();\n\n toString() {\n return `Context(${this._isDisposed ? 'disposed' : 'active'})`;\n }\n}\n", "//\n// Copyright 2023 DXOS.org\n//\n\nexport class ContextDisposedError extends Error {\n constructor() {\n super('Context disposed.');\n }\n}\n", "//\n// Copyright 2023 DXOS.org\n//\n\nimport { type Context } from './context';\nimport { ContextDisposedError } from './context-disposed-error';\n\n/**\n * @returns A promise that rejects when the context is disposed.\n */\n// TODO(dmaretskyi): Memory leak.\nexport const rejectOnDispose = (ctx: Context, error = new ContextDisposedError()): Promise<never> =>\n new Promise((resolve, reject) => {\n ctx.onDispose(() => reject(error));\n });\n\n/**\n * Rejects the promise if the context is disposed.\n */\nexport const cancelWithContext = <T>(ctx: Context, promise: Promise<T>): Promise<T> => {\n let clearDispose: () => void;\n return Promise.race([\n promise,\n new Promise<never>((resolve, reject) => {\n // Will be called before .finally() handlers.\n clearDispose = ctx.onDispose(() => reject(new ContextDisposedError()));\n }),\n ]).finally(() => clearDispose?.());\n};\n", "//\n// Copyright 2024 DXOS.org\n//\n\nimport '@dxos/util';\nimport { Context } from './context';\n\nexport interface Lifecycle {\n open?(ctx?: Context): Promise<any> | any;\n close?(): Promise<any> | any;\n}\n\nexport enum LifecycleState {\n CLOSED = 'CLOSED',\n OPEN = 'OPEN',\n ERROR = 'ERROR',\n}\n\n/**\n * Base class for resources that need to be opened and closed.\n */\nexport class Resource implements Lifecycle {\n #lifecycleState = LifecycleState.CLOSED;\n #openPromise: Promise<void> | null = null;\n #closePromise: Promise<void> | null = null;\n\n /**\n * Managed internally by the resource.\n * Recreated on close.\n * Errors are propagated to the `_catch` method and the parent context.\n */\n #internalCtx: Context = this.#createContext();\n\n /**\n * Context that is used to bubble up errors that are not handled by the resource.\n * Provided in the open method.\n */\n #parentCtx: Context = new Context();\n\n protected get _lifecycleState() {\n return this.#lifecycleState;\n }\n\n protected get _ctx() {\n return this.#internalCtx;\n }\n\n /**\n * To be overridden by subclasses.\n */\n protected async _open(ctx: Context): Promise<void> {}\n\n /**\n * To be overridden by subclasses.\n */\n protected async _close(ctx: Context): Promise<void> {}\n\n /**\n * Error handler for errors that are caught by the context.\n * By default errors are bubbled up to the parent context which is passed to the open method.\n */\n protected async _catch(err: Error): Promise<void> {\n throw err;\n }\n\n /**\n * Opens the resource.\n * If the resource is already open, it does nothing.\n * If the resource is in an error state, it throws an error.\n * If the resource is closed, it waits for it to close and then opens it.\n * @param ctx - Context to use for opening the resource. This context will receive errors that are not handled in `_catch`.\n */\n async open(ctx?: Context): Promise<this> {\n switch (this.#lifecycleState) {\n case LifecycleState.OPEN:\n return this;\n case LifecycleState.ERROR:\n throw new Error(`Invalid state: ${this.#lifecycleState}`);\n default:\n }\n await this.#closePromise;\n await (this.#openPromise ??= this.#open(ctx));\n\n return this;\n }\n\n /**\n * Closes the resource.\n * If the resource is already closed, it does nothing.\n */\n async close(ctx?: Context): Promise<this> {\n if (this.#lifecycleState === LifecycleState.CLOSED) {\n return this;\n }\n await this.#openPromise;\n await (this.#closePromise ??= this.#close(ctx));\n\n return this;\n }\n\n async [Symbol.asyncDispose]() {\n await this.close();\n }\n\n async #open(ctx?: Context) {\n this.#closePromise = null;\n if (ctx) {\n this.#parentCtx = ctx;\n }\n await this._open(this.#parentCtx);\n this.#lifecycleState = LifecycleState.OPEN;\n }\n\n async #close(ctx = new Context()) {\n this.#openPromise = null;\n await this.#internalCtx.dispose();\n await this._close(ctx);\n this.#internalCtx = this.#createContext();\n this.#lifecycleState = LifecycleState.CLOSED;\n }\n\n #createContext() {\n return new Context({\n onError: (error) =>\n queueMicrotask(async () => {\n try {\n await this._catch(error);\n } catch (err: any) {\n this.#lifecycleState = LifecycleState.ERROR;\n this.#parentCtx.raise(err);\n }\n }),\n });\n }\n}\n\nexport const openInContext = async <T extends Lifecycle>(ctx: Context, resource: T): Promise<T> => {\n await resource.open?.(ctx);\n ctx.onDispose(() => resource.close?.());\n\n return resource;\n};\n"],
5
+ "mappings": "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",
6
+ "names": ["inspect", "log", "safeInstanceof", "ContextDisposedError", "Error", "constructor", "MAX_SAFE_DISPOSE_CALLBACKS", "Context", "constructor", "onError", "error", "ContextDisposedError", "dispose", "attributes", "parent", "_disposeCallbacks", "_isDisposed", "_disposePromise", "undefined", "_parent", "maxSafeDisposeCallbacks", "Symbol", "inspect", "toString", "_onError", "_attributes", "toStringTag", "custom", "default", "disposed", "disposeCallbacksLength", "length", "onDispose", "callback", "log", "catch", "push", "warn", "count", "safeThreshold", "index", "indexOf", "splice", "resolveDispose", "Promise", "resolve", "callbacks", "Array", "from", "reverse", "raise", "err", "reject", "derive", "newCtx", "clearDispose", "getAttribute", "key", "safeInstanceof", "rejectOnDispose", "ctx", "error", "ContextDisposedError", "Promise", "resolve", "reject", "onDispose", "cancelWithContext", "promise", "clearDispose", "race", "finally", "LifecycleState", "Resource", "Context", "_lifecycleState", "_ctx", "_open", "ctx", "_close", "_catch", "err", "open", "Error", "close", "Symbol", "asyncDispose", "dispose", "onError", "error", "queueMicrotask", "raise", "openInContext", "resource", "onDispose"]
7
7
  }
@@ -1 +1 @@
1
- {"inputs":{"packages/common/context/src/context-disposed.ts":{"bytes":803,"imports":[],"format":"esm"},"packages/common/context/src/context.ts":{"bytes":18716,"imports":[{"path":"@dxos/log","kind":"import-statement","external":true},{"path":"@dxos/util","kind":"import-statement","external":true},{"path":"packages/common/context/src/context-disposed.ts","kind":"import-statement","original":"./context-disposed"}],"format":"esm"},"packages/common/context/src/promise-utils.ts":{"bytes":3026,"imports":[{"path":"packages/common/context/src/context-disposed.ts","kind":"import-statement","original":"./context-disposed"}],"format":"esm"},"packages/common/context/src/index.ts":{"bytes":675,"imports":[{"path":"packages/common/context/src/context.ts","kind":"import-statement","original":"./context"},{"path":"packages/common/context/src/promise-utils.ts","kind":"import-statement","original":"./promise-utils"},{"path":"packages/common/context/src/context-disposed.ts","kind":"import-statement","original":"./context-disposed"}],"format":"esm"}},"outputs":{"packages/common/context/dist/lib/browser/index.mjs.map":{"imports":[],"exports":[],"inputs":{},"bytes":10416},"packages/common/context/dist/lib/browser/index.mjs":{"imports":[{"path":"@dxos/log","kind":"import-statement","external":true},{"path":"@dxos/util","kind":"import-statement","external":true}],"exports":["Context","ContextDisposedError","cancelWithContext","rejectOnDispose"],"entryPoint":"packages/common/context/src/index.ts","inputs":{"packages/common/context/src/context.ts":{"bytesInOutput":5217},"packages/common/context/src/context-disposed.ts":{"bytesInOutput":106},"packages/common/context/src/index.ts":{"bytesInOutput":0},"packages/common/context/src/promise-utils.ts":{"bytesInOutput":410}},"bytes":6040}}}
1
+ {"inputs":{"packages/common/context/src/context-disposed-error.ts":{"bytes":817,"imports":[],"format":"esm"},"packages/common/context/src/context.ts":{"bytes":18959,"imports":[{"path":"@dxos/node-std/util","kind":"import-statement","external":true},{"path":"@dxos/log","kind":"import-statement","external":true},{"path":"@dxos/util","kind":"import-statement","external":true},{"path":"packages/common/context/src/context-disposed-error.ts","kind":"import-statement","original":"./context-disposed-error"}],"format":"esm"},"packages/common/context/src/promise-utils.ts":{"bytes":3040,"imports":[{"path":"packages/common/context/src/context-disposed-error.ts","kind":"import-statement","original":"./context-disposed-error"}],"format":"esm"},"packages/common/context/src/resource.ts":{"bytes":11723,"imports":[{"path":"@dxos/util","kind":"import-statement","external":true},{"path":"packages/common/context/src/context.ts","kind":"import-statement","original":"./context"}],"format":"esm"},"packages/common/context/src/index.ts":{"bytes":777,"imports":[{"path":"packages/common/context/src/context.ts","kind":"import-statement","original":"./context"},{"path":"packages/common/context/src/promise-utils.ts","kind":"import-statement","original":"./promise-utils"},{"path":"packages/common/context/src/context-disposed-error.ts","kind":"import-statement","original":"./context-disposed-error"},{"path":"packages/common/context/src/resource.ts","kind":"import-statement","original":"./resource"}],"format":"esm"}},"outputs":{"packages/common/context/dist/lib/browser/index.mjs.map":{"imports":[],"exports":[],"inputs":{},"bytes":15998},"packages/common/context/dist/lib/browser/index.mjs":{"imports":[{"path":"@dxos/node-std/util","kind":"import-statement","external":true},{"path":"@dxos/log","kind":"import-statement","external":true},{"path":"@dxos/util","kind":"import-statement","external":true},{"path":"@dxos/util","kind":"import-statement","external":true}],"exports":["Context","ContextDisposedError","LifecycleState","Resource","cancelWithContext","openInContext","rejectOnDispose"],"entryPoint":"packages/common/context/src/index.ts","inputs":{"packages/common/context/src/context.ts":{"bytesInOutput":5323},"packages/common/context/src/context-disposed-error.ts":{"bytesInOutput":106},"packages/common/context/src/index.ts":{"bytesInOutput":0},"packages/common/context/src/promise-utils.ts":{"bytesInOutput":410},"packages/common/context/src/resource.ts":{"bytesInOutput":2978}},"bytes":9221}}}
@@ -20,12 +20,17 @@ var node_exports = {};
20
20
  __export(node_exports, {
21
21
  Context: () => Context,
22
22
  ContextDisposedError: () => ContextDisposedError,
23
+ LifecycleState: () => LifecycleState,
24
+ Resource: () => Resource,
23
25
  cancelWithContext: () => cancelWithContext,
26
+ openInContext: () => openInContext,
24
27
  rejectOnDispose: () => rejectOnDispose
25
28
  });
26
29
  module.exports = __toCommonJS(node_exports);
30
+ var import_node_util = require("node:util");
27
31
  var import_log = require("@dxos/log");
28
32
  var import_util = require("@dxos/util");
33
+ var import_util2 = require("@dxos/util");
29
34
  var ContextDisposedError = class extends Error {
30
35
  constructor() {
31
36
  super("Context disposed.");
@@ -43,6 +48,7 @@ function _ts_decorate(decorators, target, key, desc) {
43
48
  }
44
49
  var __dxlog_file = "/home/runner/work/dxos/dxos/packages/common/context/src/context.ts";
45
50
  var MAX_SAFE_DISPOSE_CALLBACKS = 300;
51
+ var _a, _b;
46
52
  var Context = class _Context {
47
53
  constructor({ onError = (error) => {
48
54
  if (error instanceof ContextDisposedError) {
@@ -56,12 +62,20 @@ var Context = class _Context {
56
62
  this._disposePromise = void 0;
57
63
  this._parent = null;
58
64
  this.maxSafeDisposeCallbacks = MAX_SAFE_DISPOSE_CALLBACKS;
65
+ this[_a] = "Context";
66
+ this[_b] = () => this.toString();
59
67
  this._onError = onError;
60
68
  this._attributes = attributes;
61
69
  if (parent !== void 0) {
62
70
  this._parent = parent;
63
71
  }
64
72
  }
73
+ static {
74
+ _a = Symbol.toStringTag, _b = import_node_util.inspect.custom;
75
+ }
76
+ static default() {
77
+ return new _Context();
78
+ }
65
79
  get disposed() {
66
80
  return this._isDisposed;
67
81
  }
@@ -85,7 +99,7 @@ var Context = class _Context {
85
99
  } catch (error) {
86
100
  import_log.log.catch(error, void 0, {
87
101
  F: __dxlog_file,
88
- L: 82,
102
+ L: 88,
89
103
  S: this,
90
104
  C: (f, a) => f(...a)
91
105
  });
@@ -99,7 +113,7 @@ var Context = class _Context {
99
113
  safeThreshold: this.maxSafeDisposeCallbacks
100
114
  }, {
101
115
  F: __dxlog_file,
102
- L: 89,
116
+ L: 95,
103
117
  S: this,
104
118
  C: (f, a) => f(...a)
105
119
  });
@@ -113,13 +127,12 @@ var Context = class _Context {
113
127
  }
114
128
  /**
115
129
  * Runs all dispose callbacks.
116
- * Sync callbacks are run in the reverse order they were added.
117
- * Async callbacks are run in parallel.
130
+ * Callbacks are run in the reverse order they were added.
118
131
  * This function never throws.
119
132
  * It is safe to ignore the returned promise if the caller does not wish to wait for callbacks to complete.
120
133
  * Disposing context means that onDispose will throw an error and any errors raised will be logged and not propagated.
121
134
  */
122
- dispose() {
135
+ async dispose() {
123
136
  if (this._disposePromise) {
124
137
  return this._disposePromise;
125
138
  }
@@ -128,27 +141,21 @@ var Context = class _Context {
128
141
  this._disposePromise = new Promise((resolve) => {
129
142
  resolveDispose = resolve;
130
143
  });
131
- const promises = [];
132
144
  const callbacks = Array.from(this._disposeCallbacks).reverse();
145
+ this._disposeCallbacks.length = 0;
133
146
  for (const callback of callbacks) {
134
- promises.push((async () => {
135
- try {
136
- await callback();
137
- } catch (error) {
138
- import_log.log.catch(error, void 0, {
139
- F: __dxlog_file,
140
- L: 132,
141
- S: this,
142
- C: (f, a) => f(...a)
143
- });
144
- }
145
- })());
147
+ try {
148
+ await callback();
149
+ } catch (error) {
150
+ import_log.log.catch(error, void 0, {
151
+ F: __dxlog_file,
152
+ L: 136,
153
+ S: this,
154
+ C: (f, a) => f(...a)
155
+ });
156
+ }
146
157
  }
147
- this._disposeCallbacks.length = 0;
148
- void Promise.all(promises).then(() => {
149
- resolveDispose();
150
- });
151
- return this._disposePromise;
158
+ resolveDispose();
152
159
  }
153
160
  /**
154
161
  * Raise the error inside the context.
@@ -194,6 +201,9 @@ var Context = class _Context {
194
201
  }
195
202
  return void 0;
196
203
  }
204
+ toString() {
205
+ return `Context(${this._isDisposed ? "disposed" : "active"})`;
206
+ }
197
207
  };
198
208
  Context = _ts_decorate([
199
209
  (0, import_util.safeInstanceof)("Context")
@@ -210,11 +220,125 @@ var cancelWithContext = (ctx, promise) => {
210
220
  })
211
221
  ]).finally(() => clearDispose?.());
212
222
  };
223
+ var LifecycleState;
224
+ (function(LifecycleState2) {
225
+ LifecycleState2["CLOSED"] = "CLOSED";
226
+ LifecycleState2["OPEN"] = "OPEN";
227
+ LifecycleState2["ERROR"] = "ERROR";
228
+ })(LifecycleState || (LifecycleState = {}));
229
+ var Resource = class {
230
+ #lifecycleState = "CLOSED";
231
+ #openPromise = null;
232
+ #closePromise = null;
233
+ /**
234
+ * Managed internally by the resource.
235
+ * Recreated on close.
236
+ * Errors are propagated to the `_catch` method and the parent context.
237
+ */
238
+ #internalCtx = this.#createContext();
239
+ /**
240
+ * Context that is used to bubble up errors that are not handled by the resource.
241
+ * Provided in the open method.
242
+ */
243
+ #parentCtx = new Context();
244
+ get _lifecycleState() {
245
+ return this.#lifecycleState;
246
+ }
247
+ get _ctx() {
248
+ return this.#internalCtx;
249
+ }
250
+ /**
251
+ * To be overridden by subclasses.
252
+ */
253
+ async _open(ctx) {
254
+ }
255
+ /**
256
+ * To be overridden by subclasses.
257
+ */
258
+ async _close(ctx) {
259
+ }
260
+ /**
261
+ * Error handler for errors that are caught by the context.
262
+ * By default errors are bubbled up to the parent context which is passed to the open method.
263
+ */
264
+ async _catch(err) {
265
+ throw err;
266
+ }
267
+ /**
268
+ * Opens the resource.
269
+ * If the resource is already open, it does nothing.
270
+ * If the resource is in an error state, it throws an error.
271
+ * If the resource is closed, it waits for it to close and then opens it.
272
+ * @param ctx - Context to use for opening the resource. This context will receive errors that are not handled in `_catch`.
273
+ */
274
+ async open(ctx) {
275
+ switch (this.#lifecycleState) {
276
+ case "OPEN":
277
+ return this;
278
+ case "ERROR":
279
+ throw new Error(`Invalid state: ${this.#lifecycleState}`);
280
+ default:
281
+ }
282
+ await this.#closePromise;
283
+ await (this.#openPromise ??= this.#open(ctx));
284
+ return this;
285
+ }
286
+ /**
287
+ * Closes the resource.
288
+ * If the resource is already closed, it does nothing.
289
+ */
290
+ async close(ctx) {
291
+ if (this.#lifecycleState === "CLOSED") {
292
+ return this;
293
+ }
294
+ await this.#openPromise;
295
+ await (this.#closePromise ??= this.#close(ctx));
296
+ return this;
297
+ }
298
+ async [Symbol.asyncDispose]() {
299
+ await this.close();
300
+ }
301
+ async #open(ctx) {
302
+ this.#closePromise = null;
303
+ if (ctx) {
304
+ this.#parentCtx = ctx;
305
+ }
306
+ await this._open(this.#parentCtx);
307
+ this.#lifecycleState = "OPEN";
308
+ }
309
+ async #close(ctx = new Context()) {
310
+ this.#openPromise = null;
311
+ await this.#internalCtx.dispose();
312
+ await this._close(ctx);
313
+ this.#internalCtx = this.#createContext();
314
+ this.#lifecycleState = "CLOSED";
315
+ }
316
+ #createContext() {
317
+ return new Context({
318
+ onError: (error) => queueMicrotask(async () => {
319
+ try {
320
+ await this._catch(error);
321
+ } catch (err) {
322
+ this.#lifecycleState = "ERROR";
323
+ this.#parentCtx.raise(err);
324
+ }
325
+ })
326
+ });
327
+ }
328
+ };
329
+ var openInContext = async (ctx, resource) => {
330
+ await resource.open?.(ctx);
331
+ ctx.onDispose(() => resource.close?.());
332
+ return resource;
333
+ };
213
334
  // Annotate the CommonJS export names for ESM import in node:
214
335
  0 && (module.exports = {
215
336
  Context,
216
337
  ContextDisposedError,
338
+ LifecycleState,
339
+ Resource,
217
340
  cancelWithContext,
341
+ openInContext,
218
342
  rejectOnDispose
219
343
  });
220
344
  //# sourceMappingURL=index.cjs.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../../../src/context.ts", "../../../src/context-disposed.ts", "../../../src/promise-utils.ts"],
4
- "sourcesContent": ["//\n// Copyright 2022 DXOS.org\n//\n\nimport { log } from '@dxos/log';\nimport { safeInstanceof } from '@dxos/util';\n\nimport { ContextDisposedError } from './context-disposed';\n\nexport type ContextErrorHandler = (error: Error) => void;\n\nexport type DisposeCallback = () => void | Promise<void>;\n\nexport type CreateContextParams = {\n onError?: ContextErrorHandler;\n attributes?: Record<string, any>;\n parent?: Context;\n};\n\n/**\n * Maximum number of dispose callbacks before we start logging warnings.\n */\nconst MAX_SAFE_DISPOSE_CALLBACKS = 300;\n\n@safeInstanceof('Context')\nexport class Context {\n private readonly _onError: ContextErrorHandler;\n private readonly _disposeCallbacks: DisposeCallback[] = [];\n private _isDisposed = false;\n private _disposePromise?: Promise<void> = undefined;\n private _parent: Context | null = null;\n\n private _attributes: Record<string, any>;\n\n public maxSafeDisposeCallbacks = MAX_SAFE_DISPOSE_CALLBACKS;\n\n constructor({\n onError = (error) => {\n if (error instanceof ContextDisposedError) {\n return;\n }\n\n void this.dispose();\n\n // Will generate an unhandled rejection.\n throw error;\n },\n attributes = {},\n parent,\n }: CreateContextParams = {}) {\n this._onError = onError;\n this._attributes = attributes;\n if (parent !== undefined) {\n this._parent = parent;\n }\n }\n\n get disposed() {\n return this._isDisposed;\n }\n\n get disposeCallbacksLength() {\n return this._disposeCallbacks.length;\n }\n\n /**\n * Schedules a callback to run when the context is disposed.\n * May be async, in this case the disposer might choose to wait for all resource to released.\n * Throwing an error inside the callback will result in the error being logged, but not re-thrown.\n *\n * NOTE: Will call the callback immediately if the context is already disposed.\n *\n * @returns A function that can be used to remove the callback from the dispose list.\n */\n onDispose(callback: DisposeCallback) {\n if (this._isDisposed) {\n // Call the callback immediately if the context is already disposed.\n void (async () => {\n try {\n await callback();\n } catch (error: any) {\n log.catch(error);\n }\n })();\n }\n\n this._disposeCallbacks.push(callback);\n if (this._disposeCallbacks.length > this.maxSafeDisposeCallbacks) {\n log.warn('Context has a large number of dispose callbacks. This might be a memory leak.', {\n count: this._disposeCallbacks.length,\n safeThreshold: this.maxSafeDisposeCallbacks,\n });\n }\n\n return () => {\n const index = this._disposeCallbacks.indexOf(callback);\n if (index !== -1) {\n this._disposeCallbacks.splice(index, 1);\n }\n };\n }\n\n /**\n * Runs all dispose callbacks.\n * Sync callbacks are run in the reverse order they were added.\n * Async callbacks are run in parallel.\n * This function never throws.\n * It is safe to ignore the returned promise if the caller does not wish to wait for callbacks to complete.\n * Disposing context means that onDispose will throw an error and any errors raised will be logged and not propagated.\n */\n dispose(): Promise<void> {\n if (this._disposePromise) {\n return this._disposePromise;\n }\n this._isDisposed = true;\n\n // Set the promise before running the callbacks.\n let resolveDispose: () => void;\n this._disposePromise = new Promise<void>((resolve) => {\n resolveDispose = resolve;\n });\n\n const promises = [];\n // Clone the array so that any mutations to the original array don't affect the dispose process.\n const callbacks = Array.from(this._disposeCallbacks).reverse();\n for (const callback of callbacks) {\n promises.push(\n (async () => {\n try {\n await callback();\n } catch (error: any) {\n log.catch(error);\n }\n })(),\n );\n }\n this._disposeCallbacks.length = 0;\n\n void Promise.all(promises).then(() => {\n resolveDispose();\n });\n\n return this._disposePromise;\n }\n\n /**\n * Raise the error inside the context.\n * The error will be propagated to the error handler.\n * IF the error handler is not set, the error will dispose the context and cause an unhandled rejection.\n */\n raise(error: Error): void {\n if (this._isDisposed) {\n // TODO(dmaretskyi): Don't log those.\n // log.warn('Error in disposed context', error);\n return;\n }\n\n try {\n this._onError(error);\n } catch (err) {\n // Generate an unhandled rejection and stop the error propagation.\n void Promise.reject(err);\n }\n }\n\n derive({ onError, attributes }: CreateContextParams = {}): Context {\n const newCtx = new Context({\n // TODO(dmaretskyi): Optimize to not require allocating a new closure for every context.\n onError: async (error) => {\n if (!onError) {\n this.raise(error);\n } else {\n try {\n await onError(error);\n } catch {\n this.raise(error);\n }\n }\n },\n attributes,\n });\n const clearDispose = this.onDispose(() => newCtx.dispose());\n newCtx.onDispose(clearDispose);\n return newCtx;\n }\n\n getAttribute(key: string): any {\n if (key in this._attributes) {\n return this._attributes[key];\n }\n if (this._parent !== null) {\n return this._parent.getAttribute(key);\n }\n return undefined;\n }\n}\n", "//\n// Copyright 2023 DXOS.org\n//\n\nexport class ContextDisposedError extends Error {\n constructor() {\n super('Context disposed.');\n }\n}\n", "//\n// Copyright 2023 DXOS.org\n//\n\nimport { type Context } from './context';\nimport { ContextDisposedError } from './context-disposed';\n\n/**\n * @returns A promise that rejects when the context is disposed.\n */\n// TODO(dmaretskyi): Memory leak.\nexport const rejectOnDispose = (ctx: Context, error = new ContextDisposedError()): Promise<never> =>\n new Promise((resolve, reject) => {\n ctx.onDispose(() => reject(error));\n });\n\n/**\n * Rejects the promise if the context is disposed.\n */\nexport const cancelWithContext = <T>(ctx: Context, promise: Promise<T>): Promise<T> => {\n let clearDispose: () => void;\n return Promise.race([\n promise,\n new Promise<never>((resolve, reject) => {\n // Will be called before .finally() handlers.\n clearDispose = ctx.onDispose(() => reject(new ContextDisposedError()));\n }),\n ]).finally(() => clearDispose?.());\n};\n"],
5
- "mappings": "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",
6
- "names": ["ContextDisposedError", "Error", "constructor", "MAX_SAFE_DISPOSE_CALLBACKS", "Context", "onError", "error", "dispose", "attributes", "parent", "_disposeCallbacks", "_isDisposed", "_disposePromise", "undefined", "_parent", "maxSafeDisposeCallbacks", "_onError", "_attributes", "disposed", "disposeCallbacksLength", "length", "onDispose", "callback", "log", "catch", "push", "warn", "count", "safeThreshold", "index", "indexOf", "splice", "resolveDispose", "Promise", "resolve", "promises", "callbacks", "Array", "from", "reverse", "all", "then", "raise", "err", "reject", "derive", "newCtx", "clearDispose", "getAttribute", "key", "safeInstanceof", "rejectOnDispose", "ctx", "cancelWithContext", "promise", "race", "finally"]
3
+ "sources": ["../../../src/context.ts", "../../../src/context-disposed-error.ts", "../../../src/promise-utils.ts", "../../../src/resource.ts"],
4
+ "sourcesContent": ["//\n// Copyright 2022 DXOS.org\n//\n\nimport { inspect } from 'node:util';\n\nimport { log } from '@dxos/log';\nimport { safeInstanceof } from '@dxos/util';\n\nimport { ContextDisposedError } from './context-disposed-error';\n\nexport type ContextErrorHandler = (error: Error) => void;\n\nexport type DisposeCallback = () => any | Promise<any>;\n\nexport type CreateContextParams = {\n onError?: ContextErrorHandler;\n attributes?: Record<string, any>;\n parent?: Context;\n};\n\n/**\n * Maximum number of dispose callbacks before we start logging warnings.\n */\nconst MAX_SAFE_DISPOSE_CALLBACKS = 300;\n\n@safeInstanceof('Context')\nexport class Context {\n static default() {\n return new Context();\n }\n\n private readonly _onError: ContextErrorHandler;\n private readonly _disposeCallbacks: DisposeCallback[] = [];\n private _isDisposed = false;\n private _disposePromise?: Promise<void> = undefined;\n private _parent: Context | null = null;\n\n private _attributes: Record<string, any>;\n\n public maxSafeDisposeCallbacks = MAX_SAFE_DISPOSE_CALLBACKS;\n\n constructor({\n onError = (error) => {\n if (error instanceof ContextDisposedError) {\n return;\n }\n\n void this.dispose();\n\n // Will generate an unhandled rejection.\n throw error;\n },\n attributes = {},\n parent,\n }: CreateContextParams = {}) {\n this._onError = onError;\n this._attributes = attributes;\n if (parent !== undefined) {\n this._parent = parent;\n }\n }\n\n get disposed() {\n return this._isDisposed;\n }\n\n get disposeCallbacksLength() {\n return this._disposeCallbacks.length;\n }\n\n /**\n * Schedules a callback to run when the context is disposed.\n * May be async, in this case the disposer might choose to wait for all resource to released.\n * Throwing an error inside the callback will result in the error being logged, but not re-thrown.\n *\n * NOTE: Will call the callback immediately if the context is already disposed.\n *\n * @returns A function that can be used to remove the callback from the dispose list.\n */\n onDispose(callback: DisposeCallback) {\n if (this._isDisposed) {\n // Call the callback immediately if the context is already disposed.\n void (async () => {\n try {\n await callback();\n } catch (error: any) {\n log.catch(error);\n }\n })();\n }\n\n this._disposeCallbacks.push(callback);\n if (this._disposeCallbacks.length > this.maxSafeDisposeCallbacks) {\n log.warn('Context has a large number of dispose callbacks. This might be a memory leak.', {\n count: this._disposeCallbacks.length,\n safeThreshold: this.maxSafeDisposeCallbacks,\n });\n }\n\n return () => {\n const index = this._disposeCallbacks.indexOf(callback);\n if (index !== -1) {\n this._disposeCallbacks.splice(index, 1);\n }\n };\n }\n\n /**\n * Runs all dispose callbacks.\n * Callbacks are run in the reverse order they were added.\n * This function never throws.\n * It is safe to ignore the returned promise if the caller does not wish to wait for callbacks to complete.\n * Disposing context means that onDispose will throw an error and any errors raised will be logged and not propagated.\n */\n async dispose(): Promise<void> {\n if (this._disposePromise) {\n return this._disposePromise;\n }\n this._isDisposed = true;\n\n // Set the promise before running the callbacks.\n let resolveDispose!: () => void;\n this._disposePromise = new Promise<void>((resolve) => {\n resolveDispose = resolve;\n });\n\n // Clone the array so that any mutations to the original array don't affect the dispose process.\n const callbacks = Array.from(this._disposeCallbacks).reverse();\n this._disposeCallbacks.length = 0;\n\n for (const callback of callbacks) {\n try {\n await callback();\n } catch (error: any) {\n log.catch(error);\n }\n }\n resolveDispose();\n }\n\n /**\n * Raise the error inside the context.\n * The error will be propagated to the error handler.\n * IF the error handler is not set, the error will dispose the context and cause an unhandled rejection.\n */\n raise(error: Error): void {\n if (this._isDisposed) {\n // TODO(dmaretskyi): Don't log those.\n // log.warn('Error in disposed context', error);\n return;\n }\n\n try {\n this._onError(error);\n } catch (err) {\n // Generate an unhandled rejection and stop the error propagation.\n void Promise.reject(err);\n }\n }\n\n derive({ onError, attributes }: CreateContextParams = {}): Context {\n const newCtx = new Context({\n // TODO(dmaretskyi): Optimize to not require allocating a new closure for every context.\n onError: async (error) => {\n if (!onError) {\n this.raise(error);\n } else {\n try {\n await onError(error);\n } catch {\n this.raise(error);\n }\n }\n },\n attributes,\n });\n const clearDispose = this.onDispose(() => newCtx.dispose());\n newCtx.onDispose(clearDispose);\n return newCtx;\n }\n\n getAttribute(key: string): any {\n if (key in this._attributes) {\n return this._attributes[key];\n }\n if (this._parent !== null) {\n return this._parent.getAttribute(key);\n }\n return undefined;\n }\n\n [Symbol.toStringTag] = 'Context';\n\n [inspect.custom] = () => this.toString();\n\n toString() {\n return `Context(${this._isDisposed ? 'disposed' : 'active'})`;\n }\n}\n", "//\n// Copyright 2023 DXOS.org\n//\n\nexport class ContextDisposedError extends Error {\n constructor() {\n super('Context disposed.');\n }\n}\n", "//\n// Copyright 2023 DXOS.org\n//\n\nimport { type Context } from './context';\nimport { ContextDisposedError } from './context-disposed-error';\n\n/**\n * @returns A promise that rejects when the context is disposed.\n */\n// TODO(dmaretskyi): Memory leak.\nexport const rejectOnDispose = (ctx: Context, error = new ContextDisposedError()): Promise<never> =>\n new Promise((resolve, reject) => {\n ctx.onDispose(() => reject(error));\n });\n\n/**\n * Rejects the promise if the context is disposed.\n */\nexport const cancelWithContext = <T>(ctx: Context, promise: Promise<T>): Promise<T> => {\n let clearDispose: () => void;\n return Promise.race([\n promise,\n new Promise<never>((resolve, reject) => {\n // Will be called before .finally() handlers.\n clearDispose = ctx.onDispose(() => reject(new ContextDisposedError()));\n }),\n ]).finally(() => clearDispose?.());\n};\n", "//\n// Copyright 2024 DXOS.org\n//\n\nimport '@dxos/util';\nimport { Context } from './context';\n\nexport interface Lifecycle {\n open?(ctx?: Context): Promise<any> | any;\n close?(): Promise<any> | any;\n}\n\nexport enum LifecycleState {\n CLOSED = 'CLOSED',\n OPEN = 'OPEN',\n ERROR = 'ERROR',\n}\n\n/**\n * Base class for resources that need to be opened and closed.\n */\nexport class Resource implements Lifecycle {\n #lifecycleState = LifecycleState.CLOSED;\n #openPromise: Promise<void> | null = null;\n #closePromise: Promise<void> | null = null;\n\n /**\n * Managed internally by the resource.\n * Recreated on close.\n * Errors are propagated to the `_catch` method and the parent context.\n */\n #internalCtx: Context = this.#createContext();\n\n /**\n * Context that is used to bubble up errors that are not handled by the resource.\n * Provided in the open method.\n */\n #parentCtx: Context = new Context();\n\n protected get _lifecycleState() {\n return this.#lifecycleState;\n }\n\n protected get _ctx() {\n return this.#internalCtx;\n }\n\n /**\n * To be overridden by subclasses.\n */\n protected async _open(ctx: Context): Promise<void> {}\n\n /**\n * To be overridden by subclasses.\n */\n protected async _close(ctx: Context): Promise<void> {}\n\n /**\n * Error handler for errors that are caught by the context.\n * By default errors are bubbled up to the parent context which is passed to the open method.\n */\n protected async _catch(err: Error): Promise<void> {\n throw err;\n }\n\n /**\n * Opens the resource.\n * If the resource is already open, it does nothing.\n * If the resource is in an error state, it throws an error.\n * If the resource is closed, it waits for it to close and then opens it.\n * @param ctx - Context to use for opening the resource. This context will receive errors that are not handled in `_catch`.\n */\n async open(ctx?: Context): Promise<this> {\n switch (this.#lifecycleState) {\n case LifecycleState.OPEN:\n return this;\n case LifecycleState.ERROR:\n throw new Error(`Invalid state: ${this.#lifecycleState}`);\n default:\n }\n await this.#closePromise;\n await (this.#openPromise ??= this.#open(ctx));\n\n return this;\n }\n\n /**\n * Closes the resource.\n * If the resource is already closed, it does nothing.\n */\n async close(ctx?: Context): Promise<this> {\n if (this.#lifecycleState === LifecycleState.CLOSED) {\n return this;\n }\n await this.#openPromise;\n await (this.#closePromise ??= this.#close(ctx));\n\n return this;\n }\n\n async [Symbol.asyncDispose]() {\n await this.close();\n }\n\n async #open(ctx?: Context) {\n this.#closePromise = null;\n if (ctx) {\n this.#parentCtx = ctx;\n }\n await this._open(this.#parentCtx);\n this.#lifecycleState = LifecycleState.OPEN;\n }\n\n async #close(ctx = new Context()) {\n this.#openPromise = null;\n await this.#internalCtx.dispose();\n await this._close(ctx);\n this.#internalCtx = this.#createContext();\n this.#lifecycleState = LifecycleState.CLOSED;\n }\n\n #createContext() {\n return new Context({\n onError: (error) =>\n queueMicrotask(async () => {\n try {\n await this._catch(error);\n } catch (err: any) {\n this.#lifecycleState = LifecycleState.ERROR;\n this.#parentCtx.raise(err);\n }\n }),\n });\n }\n}\n\nexport const openInContext = async <T extends Lifecycle>(ctx: Context, resource: T): Promise<T> => {\n await resource.open?.(ctx);\n ctx.onDispose(() => resource.close?.());\n\n return resource;\n};\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAIA,uBAAwB;AAExB,iBAAoB;AACpB,kBAA+B;AGH/B,IAAAA,eAAO;AFAA,IAAMC,uBAAN,cAAmCC,MAAAA;EACxCC,cAAc;AACZ,UAAM,mBAAA;EACR;AACF;;;;;;;;;;;;ADgBA,IAAMC,6BAA6B;AAxBnC,IAAA,IAAA;AA2BO,IAAMC,UAAN,MAAMA,SAAAA;EAeXF,YAAY,EACVG,UAAU,CAACC,UAAAA;AACT,QAAIA,iBAAiBN,sBAAsB;AACzC;IACF;AAEA,SAAK,KAAKO,QAAO;AAGjB,UAAMD;EACR,GACAE,aAAa,CAAC,GACdC,OAAM,IACiB,CAAC,GAAG;AAtBZC,SAAAA,oBAAuC,CAAA;AAChDC,SAAAA,cAAc;AACdC,SAAAA,kBAAkCC;AAClCC,SAAAA,UAA0B;AAI3BC,SAAAA,0BAA0BZ;AAwJjC,SAACa,EAAAA,IAAsB;AAEvB,SAACC,EAAAA,IAAkB,MAAM,KAAKC,SAAQ;AA1IpC,SAAKC,WAAWd;AAChB,SAAKe,cAAcZ;AACnB,QAAIC,WAAWI,QAAW;AACxB,WAAKC,UAAUL;IACjB;EACF;EAmICO,OAAAA;gBAAOK,aAEPJ,KAAAA,yBAAQK;;EAtKT,OAAOC,UAAU;AACf,WAAO,IAAInB,SAAAA;EACb;EAiCA,IAAIoB,WAAW;AACb,WAAO,KAAKb;EACd;EAEA,IAAIc,yBAAyB;AAC3B,WAAO,KAAKf,kBAAkBgB;EAChC;;;;;;;;;;EAWAC,UAAUC,UAA2B;AACnC,QAAI,KAAKjB,aAAa;AAEpB,YAAM,YAAA;AACJ,YAAI;AACF,gBAAMiB,SAAAA;QACR,SAAStB,OAAY;AACnBuB,yBAAIC,MAAMxB,OAAAA,QAAAA;;;;;;QACZ;MACF,GAAA;IACF;AAEA,SAAKI,kBAAkBqB,KAAKH,QAAAA;AAC5B,QAAI,KAAKlB,kBAAkBgB,SAAS,KAAKX,yBAAyB;AAChEc,qBAAIG,KAAK,iFAAiF;QACxFC,OAAO,KAAKvB,kBAAkBgB;QAC9BQ,eAAe,KAAKnB;MACtB,GAAA;;;;;;IACF;AAEA,WAAO,MAAA;AACL,YAAMoB,QAAQ,KAAKzB,kBAAkB0B,QAAQR,QAAAA;AAC7C,UAAIO,UAAU,IAAI;AAChB,aAAKzB,kBAAkB2B,OAAOF,OAAO,CAAA;MACvC;IACF;EACF;;;;;;;;EASA,MAAM5B,UAAyB;AAC7B,QAAI,KAAKK,iBAAiB;AACxB,aAAO,KAAKA;IACd;AACA,SAAKD,cAAc;AAGnB,QAAI2B;AACJ,SAAK1B,kBAAkB,IAAI2B,QAAc,CAACC,YAAAA;AACxCF,uBAAiBE;IACnB,CAAA;AAGA,UAAMC,YAAYC,MAAMC,KAAK,KAAKjC,iBAAiB,EAAEkC,QAAO;AAC5D,SAAKlC,kBAAkBgB,SAAS;AAEhC,eAAWE,YAAYa,WAAW;AAChC,UAAI;AACF,cAAMb,SAAAA;MACR,SAAStB,OAAY;AACnBuB,uBAAIC,MAAMxB,OAAAA,QAAAA;;;;;;MACZ;IACF;AACAgC,mBAAAA;EACF;;;;;;EAOAO,MAAMvC,OAAoB;AACxB,QAAI,KAAKK,aAAa;AAGpB;IACF;AAEA,QAAI;AACF,WAAKQ,SAASb,KAAAA;IAChB,SAASwC,KAAK;AAEZ,WAAKP,QAAQQ,OAAOD,GAAAA;IACtB;EACF;EAEAE,OAAO,EAAE3C,SAASG,WAAU,IAA0B,CAAC,GAAY;AACjE,UAAMyC,SAAS,IAAI7C,SAAQ;;MAEzBC,SAAS,OAAOC,UAAAA;AACd,YAAI,CAACD,SAAS;AACZ,eAAKwC,MAAMvC,KAAAA;QACb,OAAO;AACL,cAAI;AACF,kBAAMD,QAAQC,KAAAA;UAChB,QAAQ;AACN,iBAAKuC,MAAMvC,KAAAA;UACb;QACF;MACF;MACAE;IACF,CAAA;AACA,UAAM0C,eAAe,KAAKvB,UAAU,MAAMsB,OAAO1C,QAAO,CAAA;AACxD0C,WAAOtB,UAAUuB,YAAAA;AACjB,WAAOD;EACT;EAEAE,aAAaC,KAAkB;AAC7B,QAAIA,OAAO,KAAKhC,aAAa;AAC3B,aAAO,KAAKA,YAAYgC,GAAAA;IAC1B;AACA,QAAI,KAAKtC,YAAY,MAAM;AACzB,aAAO,KAAKA,QAAQqC,aAAaC,GAAAA;IACnC;AACA,WAAOvC;EACT;EAMAK,WAAW;AACT,WAAO,WAAW,KAAKP,cAAc,aAAa,QAAA;EACpD;AACF;AA5KaP,UAAAA,aAAAA;MADZiD,4BAAe,SAAA;GACHjD,OAAAA;AEhBN,IAAMkD,kBAAkB,CAACC,KAAcjD,QAAQ,IAAIN,qBAAAA,MACxD,IAAIuC,QAAQ,CAACC,SAASO,WAAAA;AACpBQ,MAAI5B,UAAU,MAAMoB,OAAOzC,KAAAA,CAAAA;AAC7B,CAAA;AAKK,IAAMkD,oBAAoB,CAAID,KAAcE,YAAAA;AACjD,MAAIP;AACJ,SAAOX,QAAQmB,KAAK;IAClBD;IACA,IAAIlB,QAAe,CAACC,SAASO,WAAAA;AAE3BG,qBAAeK,IAAI5B,UAAU,MAAMoB,OAAO,IAAI/C,qBAAAA,CAAAA,CAAAA;IAChD,CAAA;GACD,EAAE2D,QAAQ,MAAMT,eAAAA,CAAAA;AACnB;;UChBYU,iBAAAA;;;;GAAAA,mBAAAA,iBAAAA,CAAAA,EAAAA;AASL,IAAMC,WAAN,MAAMA;EACX,kBAAe;EACf,eAAqC;EACrC,gBAAsC;;;;;;EAOtC,eAAwB,KAAK,eAAc;;;;;EAM3C,aAAsB,IAAIzD,QAAAA;EAE1B,IAAc0D,kBAAkB;AAC9B,WAAO,KAAK;EACd;EAEA,IAAcC,OAAO;AACnB,WAAO,KAAK;EACd;;;;EAKA,MAAgBC,MAAMT,KAA6B;EAAC;;;;EAKpD,MAAgBU,OAAOV,KAA6B;EAAC;;;;;EAMrD,MAAgBW,OAAOpB,KAA2B;AAChD,UAAMA;EACR;;;;;;;;EASA,MAAMqB,KAAKZ,KAA8B;AACvC,YAAQ,KAAK,iBAAe;MAC1B,KAAA;AACE,eAAO;MACT,KAAA;AACE,cAAM,IAAItD,MAAM,kBAAkB,KAAK,eAAe,EAAE;MAC1D;IACF;AACA,UAAM,KAAK;AACX,WAAO,KAAK,iBAAiB,KAAK,MAAMsD,GAAAA;AAExC,WAAO;EACT;;;;;EAMA,MAAMa,MAAMb,KAA8B;AACxC,QAAI,KAAK,oBAAe,UAA4B;AAClD,aAAO;IACT;AACA,UAAM,KAAK;AACX,WAAO,KAAK,kBAAkB,KAAK,OAAOA,GAAAA;AAE1C,WAAO;EACT;EAEA,OAAOvC,OAAOqD,YAAY,IAAI;AAC5B,UAAM,KAAKD,MAAK;EAClB;EAEA,MAAM,MAAMb,KAAa;AACvB,SAAK,gBAAgB;AACrB,QAAIA,KAAK;AACP,WAAK,aAAaA;IACpB;AACA,UAAM,KAAKS,MAAM,KAAK,UAAU;AAChC,SAAK,kBAAe;EACtB;EAEA,MAAM,OAAOT,MAAM,IAAInD,QAAAA,GAAS;AAC9B,SAAK,eAAe;AACpB,UAAM,KAAK,aAAaG,QAAO;AAC/B,UAAM,KAAK0D,OAAOV,GAAAA;AAClB,SAAK,eAAe,KAAK,eAAc;AACvC,SAAK,kBAAe;EACtB;EAEA,iBAAc;AACZ,WAAO,IAAInD,QAAQ;MACjBC,SAAS,CAACC,UACRgE,eAAe,YAAA;AACb,YAAI;AACF,gBAAM,KAAKJ,OAAO5D,KAAAA;QACpB,SAASwC,KAAU;AACjB,eAAK,kBAAe;AACpB,eAAK,WAAWD,MAAMC,GAAAA;QACxB;MACF,CAAA;IACJ,CAAA;EACF;AACF;AAEO,IAAMyB,gBAAgB,OAA4BhB,KAAciB,aAAAA;AACrE,QAAMA,SAASL,OAAOZ,GAAAA;AACtBA,MAAI5B,UAAU,MAAM6C,SAASJ,QAAK,CAAA;AAElC,SAAOI;AACT;",
6
+ "names": ["import_util", "ContextDisposedError", "Error", "constructor", "MAX_SAFE_DISPOSE_CALLBACKS", "Context", "onError", "error", "dispose", "attributes", "parent", "_disposeCallbacks", "_isDisposed", "_disposePromise", "undefined", "_parent", "maxSafeDisposeCallbacks", "Symbol", "inspect", "toString", "_onError", "_attributes", "toStringTag", "custom", "default", "disposed", "disposeCallbacksLength", "length", "onDispose", "callback", "log", "catch", "push", "warn", "count", "safeThreshold", "index", "indexOf", "splice", "resolveDispose", "Promise", "resolve", "callbacks", "Array", "from", "reverse", "raise", "err", "reject", "derive", "newCtx", "clearDispose", "getAttribute", "key", "safeInstanceof", "rejectOnDispose", "ctx", "cancelWithContext", "promise", "race", "finally", "LifecycleState", "Resource", "_lifecycleState", "_ctx", "_open", "_close", "_catch", "open", "close", "asyncDispose", "queueMicrotask", "openInContext", "resource"]
7
7
  }
@@ -1 +1 @@
1
- {"inputs":{"packages/common/context/src/context-disposed.ts":{"bytes":803,"imports":[],"format":"esm"},"packages/common/context/src/context.ts":{"bytes":18716,"imports":[{"path":"@dxos/log","kind":"import-statement","external":true},{"path":"@dxos/util","kind":"import-statement","external":true},{"path":"packages/common/context/src/context-disposed.ts","kind":"import-statement","original":"./context-disposed"}],"format":"esm"},"packages/common/context/src/promise-utils.ts":{"bytes":3026,"imports":[{"path":"packages/common/context/src/context-disposed.ts","kind":"import-statement","original":"./context-disposed"}],"format":"esm"},"packages/common/context/src/index.ts":{"bytes":675,"imports":[{"path":"packages/common/context/src/context.ts","kind":"import-statement","original":"./context"},{"path":"packages/common/context/src/promise-utils.ts","kind":"import-statement","original":"./promise-utils"},{"path":"packages/common/context/src/context-disposed.ts","kind":"import-statement","original":"./context-disposed"}],"format":"esm"}},"outputs":{"packages/common/context/dist/lib/node/index.cjs.map":{"imports":[],"exports":[],"inputs":{},"bytes":10416},"packages/common/context/dist/lib/node/index.cjs":{"imports":[{"path":"@dxos/log","kind":"import-statement","external":true},{"path":"@dxos/util","kind":"import-statement","external":true}],"exports":["Context","ContextDisposedError","cancelWithContext","rejectOnDispose"],"entryPoint":"packages/common/context/src/index.ts","inputs":{"packages/common/context/src/context.ts":{"bytesInOutput":5217},"packages/common/context/src/context-disposed.ts":{"bytesInOutput":106},"packages/common/context/src/index.ts":{"bytesInOutput":0},"packages/common/context/src/promise-utils.ts":{"bytesInOutput":410}},"bytes":6040}}}
1
+ {"inputs":{"packages/common/context/src/context-disposed-error.ts":{"bytes":817,"imports":[],"format":"esm"},"packages/common/context/src/context.ts":{"bytes":18959,"imports":[{"path":"node:util","kind":"import-statement","external":true},{"path":"@dxos/log","kind":"import-statement","external":true},{"path":"@dxos/util","kind":"import-statement","external":true},{"path":"packages/common/context/src/context-disposed-error.ts","kind":"import-statement","original":"./context-disposed-error"}],"format":"esm"},"packages/common/context/src/promise-utils.ts":{"bytes":3040,"imports":[{"path":"packages/common/context/src/context-disposed-error.ts","kind":"import-statement","original":"./context-disposed-error"}],"format":"esm"},"packages/common/context/src/resource.ts":{"bytes":11723,"imports":[{"path":"@dxos/util","kind":"import-statement","external":true},{"path":"packages/common/context/src/context.ts","kind":"import-statement","original":"./context"}],"format":"esm"},"packages/common/context/src/index.ts":{"bytes":777,"imports":[{"path":"packages/common/context/src/context.ts","kind":"import-statement","original":"./context"},{"path":"packages/common/context/src/promise-utils.ts","kind":"import-statement","original":"./promise-utils"},{"path":"packages/common/context/src/context-disposed-error.ts","kind":"import-statement","original":"./context-disposed-error"},{"path":"packages/common/context/src/resource.ts","kind":"import-statement","original":"./resource"}],"format":"esm"}},"outputs":{"packages/common/context/dist/lib/node/index.cjs.map":{"imports":[],"exports":[],"inputs":{},"bytes":15998},"packages/common/context/dist/lib/node/index.cjs":{"imports":[{"path":"node:util","kind":"import-statement","external":true},{"path":"@dxos/log","kind":"import-statement","external":true},{"path":"@dxos/util","kind":"import-statement","external":true},{"path":"@dxos/util","kind":"import-statement","external":true}],"exports":["Context","ContextDisposedError","LifecycleState","Resource","cancelWithContext","openInContext","rejectOnDispose"],"entryPoint":"packages/common/context/src/index.ts","inputs":{"packages/common/context/src/context.ts":{"bytesInOutput":5313},"packages/common/context/src/context-disposed-error.ts":{"bytesInOutput":106},"packages/common/context/src/index.ts":{"bytesInOutput":0},"packages/common/context/src/promise-utils.ts":{"bytesInOutput":410},"packages/common/context/src/resource.ts":{"bytesInOutput":2978}},"bytes":9211}}}
@@ -1,4 +1,4 @@
1
1
  export declare class ContextDisposedError extends Error {
2
2
  constructor();
3
3
  }
4
- //# sourceMappingURL=context-disposed.d.ts.map
4
+ //# sourceMappingURL=context-disposed-error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-disposed-error.d.ts","sourceRoot":"","sources":["../../../src/context-disposed-error.ts"],"names":[],"mappings":"AAIA,qBAAa,oBAAqB,SAAQ,KAAK;;CAI9C"}
@@ -1,11 +1,14 @@
1
+ /// <reference types="node" />
2
+ import { inspect } from 'node:util';
1
3
  export type ContextErrorHandler = (error: Error) => void;
2
- export type DisposeCallback = () => void | Promise<void>;
4
+ export type DisposeCallback = () => any | Promise<any>;
3
5
  export type CreateContextParams = {
4
6
  onError?: ContextErrorHandler;
5
7
  attributes?: Record<string, any>;
6
8
  parent?: Context;
7
9
  };
8
10
  export declare class Context {
11
+ static default(): Context;
9
12
  private readonly _onError;
10
13
  private readonly _disposeCallbacks;
11
14
  private _isDisposed;
@@ -28,8 +31,7 @@ export declare class Context {
28
31
  onDispose(callback: DisposeCallback): () => void;
29
32
  /**
30
33
  * Runs all dispose callbacks.
31
- * Sync callbacks are run in the reverse order they were added.
32
- * Async callbacks are run in parallel.
34
+ * Callbacks are run in the reverse order they were added.
33
35
  * This function never throws.
34
36
  * It is safe to ignore the returned promise if the caller does not wish to wait for callbacks to complete.
35
37
  * Disposing context means that onDispose will throw an error and any errors raised will be logged and not propagated.
@@ -43,5 +45,8 @@ export declare class Context {
43
45
  raise(error: Error): void;
44
46
  derive({ onError, attributes }?: CreateContextParams): Context;
45
47
  getAttribute(key: string): any;
48
+ [Symbol.toStringTag]: string;
49
+ [inspect.custom]: () => string;
50
+ toString(): string;
46
51
  }
47
52
  //# sourceMappingURL=context.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../../src/context.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,mBAAmB,GAAG,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;AAEzD,MAAM,MAAM,eAAe,GAAG,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAEzD,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAOF,qBACa,OAAO;IAClB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAsB;IAC/C,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAyB;IAC3D,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,eAAe,CAAC,CAA4B;IACpD,OAAO,CAAC,OAAO,CAAwB;IAEvC,OAAO,CAAC,WAAW,CAAsB;IAElC,uBAAuB,SAA8B;gBAEhD,EACV,OASC,EACD,UAAe,EACf,MAAM,GACP,GAAE,mBAAwB;IAQ3B,IAAI,QAAQ,YAEX;IAED,IAAI,sBAAsB,WAEzB;IAED;;;;;;;;OAQG;IACH,SAAS,CAAC,QAAQ,EAAE,eAAe;IA4BnC;;;;;;;OAOG;IACH,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAmCxB;;;;OAIG;IACH,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAezB,MAAM,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,GAAE,mBAAwB,GAAG,OAAO;IAqBlE,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG;CAS/B"}
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../../src/context.ts"],"names":[],"mappings":";AAIA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAOpC,MAAM,MAAM,mBAAmB,GAAG,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;AAEzD,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;AAEvD,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAOF,qBACa,OAAO;IAClB,MAAM,CAAC,OAAO;IAId,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAsB;IAC/C,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAyB;IAC3D,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,eAAe,CAAC,CAA4B;IACpD,OAAO,CAAC,OAAO,CAAwB;IAEvC,OAAO,CAAC,WAAW,CAAsB;IAElC,uBAAuB,SAA8B;gBAEhD,EACV,OASC,EACD,UAAe,EACf,MAAM,GACP,GAAE,mBAAwB;IAQ3B,IAAI,QAAQ,YAEX;IAED,IAAI,sBAAsB,WAEzB;IAED;;;;;;;;OAQG;IACH,SAAS,CAAC,QAAQ,EAAE,eAAe;IA4BnC;;;;;;OAMG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IA0B9B;;;;OAIG;IACH,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAezB,MAAM,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,GAAE,mBAAwB,GAAG,OAAO;IAqBlE,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG;IAU9B,CAAC,MAAM,CAAC,WAAW,CAAC,SAAa;IAEjC,CAAC,OAAO,CAAC,MAAM,CAAC,eAAyB;IAEzC,QAAQ;CAGT"}
@@ -1,4 +1,5 @@
1
1
  export * from './context';
2
2
  export * from './promise-utils';
3
- export * from './context-disposed';
3
+ export * from './context-disposed-error';
4
+ export * from './resource';
4
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAIA,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAIA,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,0BAA0B,CAAC;AACzC,cAAc,YAAY,CAAC"}
@@ -1,5 +1,5 @@
1
1
  import { type Context } from './context';
2
- import { ContextDisposedError } from './context-disposed';
2
+ import { ContextDisposedError } from './context-disposed-error';
3
3
  /**
4
4
  * @returns A promise that rejects when the context is disposed.
5
5
  */
@@ -1 +1 @@
1
- {"version":3,"file":"promise-utils.d.ts","sourceRoot":"","sources":["../../../src/promise-utils.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAE1D;;GAEG;AAEH,eAAO,MAAM,eAAe,QAAS,OAAO,mCAAuC,QAAQ,KAAK,CAG5F,CAAC;AAEL;;GAEG;AACH,eAAO,MAAM,iBAAiB,WAAY,OAAO,oCAShD,CAAC"}
1
+ {"version":3,"file":"promise-utils.d.ts","sourceRoot":"","sources":["../../../src/promise-utils.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAEhE;;GAEG;AAEH,eAAO,MAAM,eAAe,QAAS,OAAO,mCAAuC,QAAQ,KAAK,CAG5F,CAAC;AAEL;;GAEG;AACH,eAAO,MAAM,iBAAiB,WAAY,OAAO,oCAShD,CAAC"}
@@ -0,0 +1,48 @@
1
+ import '@dxos/util';
2
+ import { Context } from './context';
3
+ export interface Lifecycle {
4
+ open?(ctx?: Context): Promise<any> | any;
5
+ close?(): Promise<any> | any;
6
+ }
7
+ export declare enum LifecycleState {
8
+ CLOSED = "CLOSED",
9
+ OPEN = "OPEN",
10
+ ERROR = "ERROR"
11
+ }
12
+ /**
13
+ * Base class for resources that need to be opened and closed.
14
+ */
15
+ export declare class Resource implements Lifecycle {
16
+ #private;
17
+ protected get _lifecycleState(): LifecycleState;
18
+ protected get _ctx(): Context;
19
+ /**
20
+ * To be overridden by subclasses.
21
+ */
22
+ protected _open(ctx: Context): Promise<void>;
23
+ /**
24
+ * To be overridden by subclasses.
25
+ */
26
+ protected _close(ctx: Context): Promise<void>;
27
+ /**
28
+ * Error handler for errors that are caught by the context.
29
+ * By default errors are bubbled up to the parent context which is passed to the open method.
30
+ */
31
+ protected _catch(err: Error): Promise<void>;
32
+ /**
33
+ * Opens the resource.
34
+ * If the resource is already open, it does nothing.
35
+ * If the resource is in an error state, it throws an error.
36
+ * If the resource is closed, it waits for it to close and then opens it.
37
+ * @param ctx - Context to use for opening the resource. This context will receive errors that are not handled in `_catch`.
38
+ */
39
+ open(ctx?: Context): Promise<this>;
40
+ /**
41
+ * Closes the resource.
42
+ * If the resource is already closed, it does nothing.
43
+ */
44
+ close(ctx?: Context): Promise<this>;
45
+ [Symbol.asyncDispose](): Promise<void>;
46
+ }
47
+ export declare const openInContext: <T extends Lifecycle>(ctx: Context, resource: T) => Promise<T>;
48
+ //# sourceMappingURL=resource.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../../../src/resource.ts"],"names":[],"mappings":"AAIA,OAAO,YAAY,CAAC;AACpB,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;IACzC,KAAK,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;CAC9B;AAED,oBAAY,cAAc;IACxB,MAAM,WAAW;IACjB,IAAI,SAAS;IACb,KAAK,UAAU;CAChB;AAED;;GAEG;AACH,qBAAa,QAAS,YAAW,SAAS;;IAkBxC,SAAS,KAAK,eAAe,mBAE5B;IAED,SAAS,KAAK,IAAI,YAEjB;IAED;;OAEG;cACa,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAElD;;OAEG;cACa,MAAM,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAEnD;;;OAGG;cACa,MAAM,CAAC,GAAG,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC;IAIjD;;;;;;OAMG;IACG,IAAI,CAAC,GAAG,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAcxC;;;OAGG;IACG,KAAK,CAAC,GAAG,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAUnC,CAAC,MAAM,CAAC,YAAY,CAAC;CAkC5B;AAED,eAAO,MAAM,aAAa,6BAAoC,OAAO,4BAKpE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/context",
3
- "version": "0.4.10-main.fa5a270",
3
+ "version": "0.4.10-main.fd8ea31",
4
4
  "description": "Async utils.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -16,8 +16,9 @@
16
16
  "src"
17
17
  ],
18
18
  "dependencies": {
19
- "@dxos/log": "0.4.10-main.fa5a270",
20
- "@dxos/util": "0.4.10-main.fa5a270"
19
+ "@dxos/node-std": "0.4.10-main.fd8ea31",
20
+ "@dxos/util": "0.4.10-main.fd8ea31",
21
+ "@dxos/log": "0.4.10-main.fd8ea31"
21
22
  },
22
23
  "publishConfig": {
23
24
  "access": "public"
package/src/context.ts CHANGED
@@ -2,14 +2,16 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
+ import { inspect } from 'node:util';
6
+
5
7
  import { log } from '@dxos/log';
6
8
  import { safeInstanceof } from '@dxos/util';
7
9
 
8
- import { ContextDisposedError } from './context-disposed';
10
+ import { ContextDisposedError } from './context-disposed-error';
9
11
 
10
12
  export type ContextErrorHandler = (error: Error) => void;
11
13
 
12
- export type DisposeCallback = () => void | Promise<void>;
14
+ export type DisposeCallback = () => any | Promise<any>;
13
15
 
14
16
  export type CreateContextParams = {
15
17
  onError?: ContextErrorHandler;
@@ -24,6 +26,10 @@ const MAX_SAFE_DISPOSE_CALLBACKS = 300;
24
26
 
25
27
  @safeInstanceof('Context')
26
28
  export class Context {
29
+ static default() {
30
+ return new Context();
31
+ }
32
+
27
33
  private readonly _onError: ContextErrorHandler;
28
34
  private readonly _disposeCallbacks: DisposeCallback[] = [];
29
35
  private _isDisposed = false;
@@ -102,45 +108,35 @@ export class Context {
102
108
 
103
109
  /**
104
110
  * Runs all dispose callbacks.
105
- * Sync callbacks are run in the reverse order they were added.
106
- * Async callbacks are run in parallel.
111
+ * Callbacks are run in the reverse order they were added.
107
112
  * This function never throws.
108
113
  * It is safe to ignore the returned promise if the caller does not wish to wait for callbacks to complete.
109
114
  * Disposing context means that onDispose will throw an error and any errors raised will be logged and not propagated.
110
115
  */
111
- dispose(): Promise<void> {
116
+ async dispose(): Promise<void> {
112
117
  if (this._disposePromise) {
113
118
  return this._disposePromise;
114
119
  }
115
120
  this._isDisposed = true;
116
121
 
117
122
  // Set the promise before running the callbacks.
118
- let resolveDispose: () => void;
123
+ let resolveDispose!: () => void;
119
124
  this._disposePromise = new Promise<void>((resolve) => {
120
125
  resolveDispose = resolve;
121
126
  });
122
127
 
123
- const promises = [];
124
128
  // Clone the array so that any mutations to the original array don't affect the dispose process.
125
129
  const callbacks = Array.from(this._disposeCallbacks).reverse();
126
- for (const callback of callbacks) {
127
- promises.push(
128
- (async () => {
129
- try {
130
- await callback();
131
- } catch (error: any) {
132
- log.catch(error);
133
- }
134
- })(),
135
- );
136
- }
137
130
  this._disposeCallbacks.length = 0;
138
131
 
139
- void Promise.all(promises).then(() => {
140
- resolveDispose();
141
- });
142
-
143
- return this._disposePromise;
132
+ for (const callback of callbacks) {
133
+ try {
134
+ await callback();
135
+ } catch (error: any) {
136
+ log.catch(error);
137
+ }
138
+ }
139
+ resolveDispose();
144
140
  }
145
141
 
146
142
  /**
@@ -193,4 +189,12 @@ export class Context {
193
189
  }
194
190
  return undefined;
195
191
  }
192
+
193
+ [Symbol.toStringTag] = 'Context';
194
+
195
+ [inspect.custom] = () => this.toString();
196
+
197
+ toString() {
198
+ return `Context(${this._isDisposed ? 'disposed' : 'active'})`;
199
+ }
196
200
  }
package/src/index.ts CHANGED
@@ -4,4 +4,5 @@
4
4
 
5
5
  export * from './context';
6
6
  export * from './promise-utils';
7
- export * from './context-disposed';
7
+ export * from './context-disposed-error';
8
+ export * from './resource';
@@ -3,7 +3,7 @@
3
3
  //
4
4
 
5
5
  import { type Context } from './context';
6
- import { ContextDisposedError } from './context-disposed';
6
+ import { ContextDisposedError } from './context-disposed-error';
7
7
 
8
8
  /**
9
9
  * @returns A promise that rejects when the context is disposed.
@@ -0,0 +1,142 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import '@dxos/util';
6
+ import { Context } from './context';
7
+
8
+ export interface Lifecycle {
9
+ open?(ctx?: Context): Promise<any> | any;
10
+ close?(): Promise<any> | any;
11
+ }
12
+
13
+ export enum LifecycleState {
14
+ CLOSED = 'CLOSED',
15
+ OPEN = 'OPEN',
16
+ ERROR = 'ERROR',
17
+ }
18
+
19
+ /**
20
+ * Base class for resources that need to be opened and closed.
21
+ */
22
+ export class Resource implements Lifecycle {
23
+ #lifecycleState = LifecycleState.CLOSED;
24
+ #openPromise: Promise<void> | null = null;
25
+ #closePromise: Promise<void> | null = null;
26
+
27
+ /**
28
+ * Managed internally by the resource.
29
+ * Recreated on close.
30
+ * Errors are propagated to the `_catch` method and the parent context.
31
+ */
32
+ #internalCtx: Context = this.#createContext();
33
+
34
+ /**
35
+ * Context that is used to bubble up errors that are not handled by the resource.
36
+ * Provided in the open method.
37
+ */
38
+ #parentCtx: Context = new Context();
39
+
40
+ protected get _lifecycleState() {
41
+ return this.#lifecycleState;
42
+ }
43
+
44
+ protected get _ctx() {
45
+ return this.#internalCtx;
46
+ }
47
+
48
+ /**
49
+ * To be overridden by subclasses.
50
+ */
51
+ protected async _open(ctx: Context): Promise<void> {}
52
+
53
+ /**
54
+ * To be overridden by subclasses.
55
+ */
56
+ protected async _close(ctx: Context): Promise<void> {}
57
+
58
+ /**
59
+ * Error handler for errors that are caught by the context.
60
+ * By default errors are bubbled up to the parent context which is passed to the open method.
61
+ */
62
+ protected async _catch(err: Error): Promise<void> {
63
+ throw err;
64
+ }
65
+
66
+ /**
67
+ * Opens the resource.
68
+ * If the resource is already open, it does nothing.
69
+ * If the resource is in an error state, it throws an error.
70
+ * If the resource is closed, it waits for it to close and then opens it.
71
+ * @param ctx - Context to use for opening the resource. This context will receive errors that are not handled in `_catch`.
72
+ */
73
+ async open(ctx?: Context): Promise<this> {
74
+ switch (this.#lifecycleState) {
75
+ case LifecycleState.OPEN:
76
+ return this;
77
+ case LifecycleState.ERROR:
78
+ throw new Error(`Invalid state: ${this.#lifecycleState}`);
79
+ default:
80
+ }
81
+ await this.#closePromise;
82
+ await (this.#openPromise ??= this.#open(ctx));
83
+
84
+ return this;
85
+ }
86
+
87
+ /**
88
+ * Closes the resource.
89
+ * If the resource is already closed, it does nothing.
90
+ */
91
+ async close(ctx?: Context): Promise<this> {
92
+ if (this.#lifecycleState === LifecycleState.CLOSED) {
93
+ return this;
94
+ }
95
+ await this.#openPromise;
96
+ await (this.#closePromise ??= this.#close(ctx));
97
+
98
+ return this;
99
+ }
100
+
101
+ async [Symbol.asyncDispose]() {
102
+ await this.close();
103
+ }
104
+
105
+ async #open(ctx?: Context) {
106
+ this.#closePromise = null;
107
+ if (ctx) {
108
+ this.#parentCtx = ctx;
109
+ }
110
+ await this._open(this.#parentCtx);
111
+ this.#lifecycleState = LifecycleState.OPEN;
112
+ }
113
+
114
+ async #close(ctx = new Context()) {
115
+ this.#openPromise = null;
116
+ await this.#internalCtx.dispose();
117
+ await this._close(ctx);
118
+ this.#internalCtx = this.#createContext();
119
+ this.#lifecycleState = LifecycleState.CLOSED;
120
+ }
121
+
122
+ #createContext() {
123
+ return new Context({
124
+ onError: (error) =>
125
+ queueMicrotask(async () => {
126
+ try {
127
+ await this._catch(error);
128
+ } catch (err: any) {
129
+ this.#lifecycleState = LifecycleState.ERROR;
130
+ this.#parentCtx.raise(err);
131
+ }
132
+ }),
133
+ });
134
+ }
135
+ }
136
+
137
+ export const openInContext = async <T extends Lifecycle>(ctx: Context, resource: T): Promise<T> => {
138
+ await resource.open?.(ctx);
139
+ ctx.onDispose(() => resource.close?.());
140
+
141
+ return resource;
142
+ };
@@ -1 +0,0 @@
1
- {"version":3,"file":"context-disposed.d.ts","sourceRoot":"","sources":["../../../src/context-disposed.ts"],"names":[],"mappings":"AAIA,qBAAa,oBAAqB,SAAQ,KAAK;;CAI9C"}