@dxos/context 0.4.8-next.fff1521 → 0.4.8

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.
@@ -103,15 +103,20 @@ var Context = class _Context {
103
103
  return this._disposePromise;
104
104
  }
105
105
  this._isDisposed = true;
106
+ let resolveDispose;
107
+ this._disposePromise = new Promise((resolve) => {
108
+ resolveDispose = resolve;
109
+ });
106
110
  const promises = [];
107
- for (const callback of this._disposeCallbacks.reverse()) {
111
+ const callbacks = Array.from(this._disposeCallbacks).reverse();
112
+ for (const callback of callbacks) {
108
113
  promises.push((async () => {
109
114
  try {
110
115
  await callback();
111
116
  } catch (error) {
112
117
  log.catch(error, void 0, {
113
118
  F: __dxlog_file,
114
- L: 124,
119
+ L: 132,
115
120
  S: this,
116
121
  C: (f, a) => f(...a)
117
122
  });
@@ -119,8 +124,10 @@ var Context = class _Context {
119
124
  })());
120
125
  }
121
126
  this._disposeCallbacks.length = 0;
122
- return this._disposePromise = Promise.all(promises).then(() => {
127
+ void Promise.all(promises).then(() => {
128
+ resolveDispose();
123
129
  });
130
+ return this._disposePromise;
124
131
  }
125
132
  /**
126
133
  * Raise the error inside the context.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
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 const promises = [];\n for (const callback of this._disposeCallbacks.reverse()) {\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 return (this._disposePromise = Promise.all(promises).then(() => {}));\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", "promises", "reverse", "Promise", "all", "then", "raise", "err", "reject", "derive", "newCtx", "clearDispose", "getAttribute", "key", "safeInstanceof", "rejectOnDispose", "ctx", "error", "ContextDisposedError", "Promise", "resolve", "reject", "onDispose", "cancelWithContext", "promise", "clearDispose", "race", "finally"]
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"]
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":17532,"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":9839},"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":5022},"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":5845}}}
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}}}
@@ -124,15 +124,20 @@ var Context = class _Context {
124
124
  return this._disposePromise;
125
125
  }
126
126
  this._isDisposed = true;
127
+ let resolveDispose;
128
+ this._disposePromise = new Promise((resolve) => {
129
+ resolveDispose = resolve;
130
+ });
127
131
  const promises = [];
128
- for (const callback of this._disposeCallbacks.reverse()) {
132
+ const callbacks = Array.from(this._disposeCallbacks).reverse();
133
+ for (const callback of callbacks) {
129
134
  promises.push((async () => {
130
135
  try {
131
136
  await callback();
132
137
  } catch (error) {
133
138
  import_log.log.catch(error, void 0, {
134
139
  F: __dxlog_file,
135
- L: 124,
140
+ L: 132,
136
141
  S: this,
137
142
  C: (f, a) => f(...a)
138
143
  });
@@ -140,8 +145,10 @@ var Context = class _Context {
140
145
  })());
141
146
  }
142
147
  this._disposeCallbacks.length = 0;
143
- return this._disposePromise = Promise.all(promises).then(() => {
148
+ void Promise.all(promises).then(() => {
149
+ resolveDispose();
144
150
  });
151
+ return this._disposePromise;
145
152
  }
146
153
  /**
147
154
  * Raise the error inside the context.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
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 const promises = [];\n for (const callback of this._disposeCallbacks.reverse()) {\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 return (this._disposePromise = Promise.all(promises).then(() => {}));\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", "promises", "reverse", "Promise", "all", "then", "raise", "err", "reject", "derive", "newCtx", "clearDispose", "getAttribute", "key", "safeInstanceof", "rejectOnDispose", "ctx", "resolve", "cancelWithContext", "promise", "race", "finally"]
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"]
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":17532,"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":9839},"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":5022},"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":5845}}}
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 +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;IAuBxB;;;;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":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/context",
3
- "version": "0.4.8-next.fff1521",
3
+ "version": "0.4.8",
4
4
  "description": "Async utils.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -16,8 +16,8 @@
16
16
  "src"
17
17
  ],
18
18
  "dependencies": {
19
- "@dxos/log": "0.4.8-next.fff1521",
20
- "@dxos/util": "0.4.8-next.fff1521"
19
+ "@dxos/log": "0.4.8",
20
+ "@dxos/util": "0.4.8"
21
21
  },
22
22
  "publishConfig": {
23
23
  "access": "public"
@@ -58,4 +58,39 @@ describe('Context', () => {
58
58
  void ctx.dispose();
59
59
  expect(called).toBeTruthy();
60
60
  });
61
+
62
+ test('canceling a context with derived contexts calls dispose hooks', async () => {
63
+ let childCalled = false;
64
+ let parentCalled = false;
65
+
66
+ const parentCtx = new Context();
67
+ parentCtx.onDispose(() => {
68
+ parentCalled = true;
69
+ });
70
+
71
+ const childCtx = parentCtx.derive();
72
+ childCtx.onDispose(() => {
73
+ childCalled = true;
74
+ });
75
+
76
+ await parentCtx.dispose();
77
+ expect(parentCalled).toBeTruthy();
78
+ expect(childCalled).toBeTruthy();
79
+ });
80
+
81
+ test('callbacks are called in reverse order', async () => {
82
+ const ctx = new Context();
83
+
84
+ const order: number[] = [];
85
+ ctx.onDispose(() => {
86
+ order.push(1);
87
+ });
88
+
89
+ ctx.onDispose(() => {
90
+ order.push(2);
91
+ });
92
+
93
+ await ctx.dispose();
94
+ expect(order).toEqual([2, 1]);
95
+ });
61
96
  });
package/src/context.ts CHANGED
@@ -114,8 +114,16 @@ export class Context {
114
114
  }
115
115
  this._isDisposed = true;
116
116
 
117
+ // Set the promise before running the callbacks.
118
+ let resolveDispose: () => void;
119
+ this._disposePromise = new Promise<void>((resolve) => {
120
+ resolveDispose = resolve;
121
+ });
122
+
117
123
  const promises = [];
118
- for (const callback of this._disposeCallbacks.reverse()) {
124
+ // Clone the array so that any mutations to the original array don't affect the dispose process.
125
+ const callbacks = Array.from(this._disposeCallbacks).reverse();
126
+ for (const callback of callbacks) {
119
127
  promises.push(
120
128
  (async () => {
121
129
  try {
@@ -128,7 +136,11 @@ export class Context {
128
136
  }
129
137
  this._disposeCallbacks.length = 0;
130
138
 
131
- return (this._disposePromise = Promise.all(promises).then(() => {}));
139
+ void Promise.all(promises).then(() => {
140
+ resolveDispose();
141
+ });
142
+
143
+ return this._disposePromise;
132
144
  }
133
145
 
134
146
  /**