@dxos/effect 0.8.3 → 0.8.4-main.16b68245aa

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.
Files changed (91) hide show
  1. package/dist/lib/browser/chunk-CGS2ULMK.mjs +11 -0
  2. package/dist/lib/browser/chunk-CGS2ULMK.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +536 -154
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/testing.mjs +31 -0
  7. package/dist/lib/browser/testing.mjs.map +7 -0
  8. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +11 -0
  9. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +7 -0
  10. package/dist/lib/node-esm/index.mjs +536 -154
  11. package/dist/lib/node-esm/index.mjs.map +4 -4
  12. package/dist/lib/node-esm/meta.json +1 -1
  13. package/dist/lib/node-esm/testing.mjs +31 -0
  14. package/dist/lib/node-esm/testing.mjs.map +7 -0
  15. package/dist/types/src/Performance.d.ts +25 -0
  16. package/dist/types/src/Performance.d.ts.map +1 -0
  17. package/dist/types/src/RuntimeProvider.d.ts +21 -0
  18. package/dist/types/src/RuntimeProvider.d.ts.map +1 -0
  19. package/dist/types/src/ast.d.ts +42 -18
  20. package/dist/types/src/ast.d.ts.map +1 -1
  21. package/dist/types/src/async-task-tagging.d.ts +6 -0
  22. package/dist/types/src/async-task-tagging.d.ts.map +1 -0
  23. package/dist/types/src/atom-kvs.d.ts +19 -0
  24. package/dist/types/src/atom-kvs.d.ts.map +1 -0
  25. package/dist/types/src/context.d.ts +5 -0
  26. package/dist/types/src/context.d.ts.map +1 -0
  27. package/dist/types/src/dynamic-runtime.d.ts +56 -0
  28. package/dist/types/src/dynamic-runtime.d.ts.map +1 -0
  29. package/dist/types/src/dynamic-runtime.test.d.ts +2 -0
  30. package/dist/types/src/dynamic-runtime.test.d.ts.map +1 -0
  31. package/dist/types/src/errors.d.ts +57 -0
  32. package/dist/types/src/errors.d.ts.map +1 -0
  33. package/dist/types/src/errors.test.d.ts +2 -0
  34. package/dist/types/src/errors.test.d.ts.map +1 -0
  35. package/dist/types/src/index.d.ts +10 -1
  36. package/dist/types/src/index.d.ts.map +1 -1
  37. package/dist/types/src/interrupt.test.d.ts +2 -0
  38. package/dist/types/src/interrupt.test.d.ts.map +1 -0
  39. package/dist/types/src/{jsonPath.d.ts → json-path.d.ts} +12 -4
  40. package/dist/types/src/json-path.d.ts.map +1 -0
  41. package/dist/types/src/json-path.test.d.ts +2 -0
  42. package/dist/types/src/json-path.test.d.ts.map +1 -0
  43. package/dist/types/src/layers.test.d.ts +2 -0
  44. package/dist/types/src/layers.test.d.ts.map +1 -0
  45. package/dist/types/src/otel.d.ts +17 -0
  46. package/dist/types/src/otel.d.ts.map +1 -0
  47. package/dist/types/src/otel.test.d.ts +2 -0
  48. package/dist/types/src/otel.test.d.ts.map +1 -0
  49. package/dist/types/src/resource.d.ts +8 -0
  50. package/dist/types/src/resource.d.ts.map +1 -0
  51. package/dist/types/src/resource.test.d.ts +2 -0
  52. package/dist/types/src/resource.test.d.ts.map +1 -0
  53. package/dist/types/src/testing.d.ts +58 -0
  54. package/dist/types/src/testing.d.ts.map +1 -0
  55. package/dist/types/src/types.d.ts +8 -0
  56. package/dist/types/src/types.d.ts.map +1 -0
  57. package/dist/types/src/url.d.ts +3 -1
  58. package/dist/types/src/url.d.ts.map +1 -1
  59. package/dist/types/tsconfig.tsbuildinfo +1 -1
  60. package/package.json +33 -11
  61. package/src/Performance.ts +45 -0
  62. package/src/RuntimeProvider.ts +35 -0
  63. package/src/ast.test.ts +55 -10
  64. package/src/ast.ts +151 -84
  65. package/src/async-task-tagging.ts +51 -0
  66. package/src/atom-kvs.ts +35 -0
  67. package/src/context.ts +16 -0
  68. package/src/dynamic-runtime.test.ts +465 -0
  69. package/src/dynamic-runtime.ts +195 -0
  70. package/src/errors.test.ts +22 -0
  71. package/src/errors.ts +252 -0
  72. package/src/index.ts +10 -1
  73. package/src/interrupt.test.ts +35 -0
  74. package/src/{jsonPath.test.ts → json-path.test.ts} +47 -8
  75. package/src/{jsonPath.ts → json-path.ts} +29 -4
  76. package/src/layers.test.ts +112 -0
  77. package/src/otel.test.ts +126 -0
  78. package/src/otel.ts +45 -0
  79. package/src/resource.test.ts +32 -0
  80. package/src/resource.ts +30 -0
  81. package/src/sanity.test.ts +30 -15
  82. package/src/testing.ts +86 -0
  83. package/src/types.ts +11 -0
  84. package/src/url.test.ts +1 -1
  85. package/src/url.ts +5 -2
  86. package/dist/lib/node/index.cjs +0 -487
  87. package/dist/lib/node/index.cjs.map +0 -7
  88. package/dist/lib/node/meta.json +0 -1
  89. package/dist/types/src/jsonPath.d.ts.map +0 -1
  90. package/dist/types/src/jsonPath.test.d.ts +0 -2
  91. package/dist/types/src/jsonPath.test.d.ts.map +0 -1
@@ -0,0 +1,195 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Context from 'effect/Context';
6
+ import * as Effect from 'effect/Effect';
7
+ import * as Exit from 'effect/Exit';
8
+ import type * as Fiber from 'effect/Fiber';
9
+ import type * as ManagedRuntime from 'effect/ManagedRuntime';
10
+ import * as Option from 'effect/Option';
11
+ import * as Runtime from 'effect/Runtime';
12
+
13
+ import { unwrapExit } from './errors';
14
+
15
+ /**
16
+ * Helper type to construct a union of tag identifiers from an array of tags.
17
+ */
18
+ export type TagsToContext<Tags extends ReadonlyArray<Context.Tag<any, any>>> = Tags extends readonly [
19
+ infer Head,
20
+ ...infer Tail,
21
+ ]
22
+ ? Head extends Context.Tag<infer Id, any>
23
+ ? Tail extends ReadonlyArray<Context.Tag<any, any>>
24
+ ? Id | TagsToContext<Tail>
25
+ : Id
26
+ : never
27
+ : never;
28
+
29
+ /**
30
+ * A runtime wrapper that validates required tags are available at runtime
31
+ * while providing type-level guarantees that effects require those tags.
32
+ */
33
+ export interface DynamicRuntime<Tags extends ReadonlyArray<Context.Tag<any, any>>> {
34
+ /**
35
+ * Run an effect as a promise that requires the specified tags.
36
+ */
37
+ readonly runPromise: <A, E>(effect: Effect.Effect<A, E, TagsToContext<Tags>>) => Promise<A>;
38
+
39
+ /**
40
+ * Run an effect synchronously that requires the specified tags.
41
+ */
42
+ readonly runSync: <A, E>(effect: Effect.Effect<A, E, TagsToContext<Tags>>) => A;
43
+
44
+ /**
45
+ * Run an effect synchronously returning exit that requires the specified tags.
46
+ */
47
+ readonly runSyncExit: <A, E>(effect: Effect.Effect<A, E, TagsToContext<Tags>>) => Exit.Exit<A, E>;
48
+
49
+ /**
50
+ * Run an effect as a promise returning exit that requires the specified tags.
51
+ */
52
+ readonly runPromiseExit: <A, E>(effect: Effect.Effect<A, E, TagsToContext<Tags>>) => Promise<Exit.Exit<A, E>>;
53
+
54
+ /**
55
+ * Fork an effect that requires the specified tags.
56
+ */
57
+ readonly runFork: <A, E>(effect: Effect.Effect<A, E, TagsToContext<Tags>>) => Fiber.RuntimeFiber<A, E>;
58
+
59
+ /**
60
+ * Get the runtime as an effect that requires the specified tags.
61
+ */
62
+ readonly runtimeEffect: Effect.Effect<Runtime.Runtime<TagsToContext<Tags>>, never, never>;
63
+
64
+ /**
65
+ * Dispose the underlying managed runtime.
66
+ */
67
+ readonly dispose: () => Promise<void>;
68
+
69
+ /**
70
+ * Get the underlying managed runtime.
71
+ */
72
+ readonly managedRuntime: ManagedRuntime.ManagedRuntime<any, any>;
73
+ }
74
+
75
+ /**
76
+ * Validate that all required tags are present in the runtime context.
77
+ */
78
+ const validateTags = <Tags extends ReadonlyArray<Context.Tag<any, any>>>(
79
+ context: Context.Context<any>,
80
+ tags: Tags,
81
+ ): Effect.Effect<void> =>
82
+ Effect.gen(function* () {
83
+ const missingTags: string[] = [];
84
+ for (const tag of tags) {
85
+ const option = Context.getOption(context, tag);
86
+ if (Option.isNone(option)) {
87
+ missingTags.push(tag.key);
88
+ }
89
+ }
90
+
91
+ if (missingTags.length > 0) {
92
+ return yield* Effect.die(new Error(`Missing required tags in runtime: ${missingTags.join(', ')}`));
93
+ }
94
+ });
95
+
96
+ /**
97
+ * Create a dynamic runtime from a managed runtime and validate required tags.
98
+ */
99
+ export function make<const Tags extends ReadonlyArray<Context.Tag<any, any>>>(
100
+ managedRuntime: ManagedRuntime.ManagedRuntime<any, any> | ManagedRuntime.ManagedRuntime<never, never>,
101
+ tags: Tags,
102
+ ): DynamicRuntime<Tags> {
103
+ type RequiredContext = TagsToContext<Tags>;
104
+ const managedRuntimeAny = managedRuntime as ManagedRuntime.ManagedRuntime<any, any>;
105
+
106
+ // Cache for the validated runtime - once resolved, can be used synchronously.
107
+ let cachedRuntime: Runtime.Runtime<RequiredContext> | undefined;
108
+
109
+ // Cache validated runtime for async operations.
110
+ let validatedRuntimePromise: Promise<Runtime.Runtime<RequiredContext>> | undefined;
111
+
112
+ const getValidatedRuntimeAsync = async (): Promise<Runtime.Runtime<RequiredContext>> => {
113
+ if (!validatedRuntimePromise) {
114
+ validatedRuntimePromise = managedRuntimeAny.runPromise(
115
+ Effect.gen(function* () {
116
+ const rt = yield* managedRuntimeAny.runtimeEffect;
117
+ yield* validateTags(rt.context, tags);
118
+ return rt as Runtime.Runtime<RequiredContext>;
119
+ }),
120
+ );
121
+ }
122
+ return validatedRuntimePromise;
123
+ };
124
+
125
+ // Get validated runtime for sync operations.
126
+ const getValidatedRuntime = (): Runtime.Runtime<RequiredContext> => {
127
+ const validationExit = managedRuntimeAny.runSyncExit(
128
+ Effect.gen(function* () {
129
+ const rt = yield* managedRuntimeAny.runtimeEffect;
130
+ yield* validateTags(rt.context, tags);
131
+ return rt as Runtime.Runtime<RequiredContext>;
132
+ }),
133
+ );
134
+ return unwrapExit(validationExit);
135
+ };
136
+
137
+ return {
138
+ managedRuntime: managedRuntimeAny,
139
+ runPromise: async <A, E>(effect: Effect.Effect<A, E, RequiredContext>): Promise<A> => {
140
+ const runtime = await getValidatedRuntimeAsync();
141
+ return Runtime.runPromise(runtime)(effect);
142
+ },
143
+ runSync: <A, E>(effect: Effect.Effect<A, E, RequiredContext>): A => {
144
+ const runtime = getValidatedRuntime();
145
+ return Runtime.runSync(runtime)(effect);
146
+ },
147
+ runSyncExit: <A, E>(effect: Effect.Effect<A, E, RequiredContext>): Exit.Exit<A, E> => {
148
+ const validationExit = managedRuntimeAny.runSyncExit(
149
+ Effect.gen(function* () {
150
+ const rt = yield* managedRuntimeAny.runtimeEffect;
151
+ yield* validateTags(rt.context, tags);
152
+ return rt as Runtime.Runtime<RequiredContext>;
153
+ }),
154
+ );
155
+ if (Exit.isSuccess(validationExit)) {
156
+ const runtime = validationExit.value;
157
+ return Runtime.runSyncExit(runtime)(effect);
158
+ }
159
+ return validationExit as Exit.Exit<A, E>;
160
+ },
161
+ runPromiseExit: async <A, E>(effect: Effect.Effect<A, E, RequiredContext>): Promise<Exit.Exit<A, E>> => {
162
+ try {
163
+ const runtime = await getValidatedRuntimeAsync();
164
+ return Runtime.runPromiseExit(runtime)(effect);
165
+ } catch (error) {
166
+ // If validation failed, return a failure exit
167
+ return Exit.die(error);
168
+ }
169
+ },
170
+ runFork: <A, E>(effect: Effect.Effect<A, E, RequiredContext>): Fiber.RuntimeFiber<A, E> => {
171
+ const runtime = getValidatedRuntime();
172
+ return Runtime.runFork(runtime)(effect);
173
+ },
174
+ runtimeEffect: Effect.gen(function* () {
175
+ // Return cached runtime if available.
176
+ if (cachedRuntime) {
177
+ return cachedRuntime;
178
+ }
179
+ const rt = yield* managedRuntimeAny.runtimeEffect;
180
+ yield* validateTags(rt.context, tags);
181
+ const runtime = rt as Runtime.Runtime<RequiredContext>;
182
+ // Cache for future sync calls.
183
+ cachedRuntime = runtime;
184
+ return runtime;
185
+ }).pipe(
186
+ Effect.catchAll(() =>
187
+ // This should never happen since validateTags uses Effect.die
188
+ Effect.die(new Error('Unexpected error in runtimeEffect validation')),
189
+ ),
190
+ ),
191
+ dispose: async (): Promise<void> => {
192
+ await managedRuntimeAny.dispose();
193
+ },
194
+ };
195
+ }
@@ -0,0 +1,22 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Data from 'effect/Data';
6
+ import { test } from 'vitest';
7
+
8
+ class MyError extends Data.TaggedError('MyError')<{
9
+ message: string;
10
+ }> {
11
+ constructor() {
12
+ super({ message: 'My error message' });
13
+ }
14
+ }
15
+
16
+ // Experimenting with error formatting:
17
+ // - If the error doesn't have the message set, vitest will print the error as a JS object.
18
+ // - If the error has non-empty message, vitest will pretty-print the error.
19
+ test.skip('Data error formatting', () => {
20
+ console.log(JSON.stringify(new MyError(), null, 2));
21
+ throw new MyError();
22
+ });
package/src/errors.ts ADDED
@@ -0,0 +1,252 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Cause from 'effect/Cause';
6
+ import * as Chunk from 'effect/Chunk';
7
+ import * as Effect from 'effect/Effect';
8
+ import * as Exit from 'effect/Exit';
9
+ import * as GlobalValue from 'effect/GlobalValue';
10
+ import * as Option from 'effect/Option';
11
+ import * as Runtime from 'effect/Runtime';
12
+ import type * as Tracer from 'effect/Tracer';
13
+
14
+ const spanSymbol = Symbol.for('effect/SpanAnnotation');
15
+ const spanToTrace = GlobalValue.globalValue('effect/Tracer/spanToTrace', () => new WeakMap());
16
+ const locationRegex = /\((.*)\)/g;
17
+
18
+ /**
19
+ * Adds effect spans.
20
+ * Removes effect internal functions.
21
+ * Unwraps error proxy.
22
+ */
23
+ const prettyErrorStack = (error: any, appendStacks: string[] = []): any => {
24
+ if (typeof error !== 'object' || error === null) {
25
+ return error;
26
+ }
27
+
28
+ const span = error[spanSymbol];
29
+
30
+ const lines = typeof error.stack === 'string' ? error.stack.split('\n') : [];
31
+ const out = [];
32
+
33
+ // Very hacky way to remove effect runtime internal stack frames.
34
+ let atStack = false,
35
+ inCore = false,
36
+ passedScheduler = false;
37
+ for (let i = 0; i < lines.length; i++) {
38
+ if (!atStack && !lines[i].startsWith(' at ')) {
39
+ out.push(lines[i]);
40
+ continue;
41
+ }
42
+ atStack = true;
43
+
44
+ if (lines[i].includes(' at new BaseEffectError') || lines[i].includes(' at new YieldableError')) {
45
+ i++;
46
+ continue;
47
+ }
48
+ if (lines[i].includes('Generator.next')) {
49
+ break;
50
+ }
51
+ if (lines[i].includes('effect_internal_function')) {
52
+ break;
53
+ }
54
+
55
+ const filename = lines[i].match(/\/([a-zA-Z0-9_\-.]+):\d+:\d+\)$/)?.[1];
56
+
57
+ if (!inCore && ['core-effect.ts'].includes(filename)) {
58
+ inCore = true;
59
+ }
60
+
61
+ if (inCore && !passedScheduler && ['Scheduler.ts'].includes(filename)) {
62
+ passedScheduler = true;
63
+ continue;
64
+ }
65
+
66
+ if (passedScheduler && !['Scheduler.ts'].includes(filename)) {
67
+ inCore = false;
68
+ }
69
+
70
+ if (inCore) {
71
+ continue;
72
+ }
73
+
74
+ out.push(
75
+ lines[i]
76
+ .replace(/at .*effect_instruction_i.*\((.*)\)/, 'at $1')
77
+ .replace(/EffectPrimitive\.\w+/, '<anonymous>')
78
+ .replace(/at Arguments\./, 'at '),
79
+ );
80
+ }
81
+
82
+ if (span) {
83
+ let current: Tracer.Span | Tracer.AnySpan | undefined = span;
84
+ let i = 0;
85
+ while (current && current._tag === 'Span' && i < 10) {
86
+ const stackFn = spanToTrace.get(current);
87
+ if (typeof stackFn === 'function') {
88
+ const stack = stackFn();
89
+ if (typeof stack === 'string') {
90
+ const locationMatchAll = stack.matchAll(locationRegex);
91
+ let match = false;
92
+ for (const [, location] of locationMatchAll) {
93
+ match = true;
94
+ out.push(` at ${current.name} (${location})`);
95
+ }
96
+ if (!match) {
97
+ out.push(` at ${current.name} (${stack.replace(/^at /, '')})`);
98
+ }
99
+ } else {
100
+ out.push(` at ${current.name}`);
101
+ }
102
+ } else {
103
+ out.push(` at ${current.name}`);
104
+ }
105
+ current = Option.getOrUndefined(current.parent);
106
+ i++;
107
+ }
108
+ }
109
+
110
+ out.push(...appendStacks);
111
+
112
+ error = Cause.originalError(error);
113
+ if (error.cause) {
114
+ error.cause = prettyErrorStack(error.cause);
115
+ }
116
+
117
+ Object.defineProperty(error, 'stack', {
118
+ value: out.join('\n'),
119
+ writable: true,
120
+ enumerable: false,
121
+ configurable: true,
122
+ });
123
+
124
+ return error;
125
+ };
126
+
127
+ /**
128
+ * Converts a cause to an error.
129
+ * Inserts effect spans as stack frames.
130
+ * The error will have stack frames of where the effect was run (if stack trace limit allows).
131
+ * Removes effect runtime internal stack frames.
132
+ *
133
+ * To be used in place of `Effect.runPromise`.
134
+ *
135
+ * @throws AggregateError if there are multiple errors.
136
+ */
137
+ export const causeToError = (cause: Cause.Cause<any>): Error => {
138
+ if (Cause.isEmpty(cause)) {
139
+ return new Error('Fiber failed without a cause');
140
+ } else if (Cause.isInterruptedOnly(cause)) {
141
+ return new Error('Fiber was interrupted');
142
+ } else {
143
+ const errors = [...Chunk.toArray(Cause.failures(cause)), ...Chunk.toArray(Cause.defects(cause))];
144
+
145
+ const getStackFrames = (): string[] => {
146
+ // Bun requies the target object for `captureStackTrace` to be an Error.
147
+ const o = new Error();
148
+ Error.captureStackTrace(o, causeToError);
149
+ return o.stack!.split('\n').slice(1);
150
+ };
151
+
152
+ const stackFrames = getStackFrames();
153
+ const newErrors = errors.map((error) => prettyErrorStack(error, stackFrames));
154
+
155
+ if (newErrors.length === 1) {
156
+ return newErrors[0];
157
+ } else {
158
+ return new AggregateError(newErrors);
159
+ }
160
+ }
161
+ };
162
+
163
+ /**
164
+ * Throws an error based on the cause.
165
+ * Inserts effect spans as stack frames.
166
+ * The error will have stack frames of where the effect was run (if stack trace limit allows).
167
+ * Removes effect runtime internal stack frames.
168
+ *
169
+ * To be used in place of `Effect.runPromise`.
170
+ *
171
+ * @throws AggregateError if there are multiple errors.
172
+ */
173
+ export const throwCause = (cause: Cause.Cause<any>): never => {
174
+ throw causeToError(cause);
175
+ };
176
+
177
+ export const unwrapExit = <A>(exit: Exit.Exit<A, any>): A => {
178
+ if (Exit.isSuccess(exit)) {
179
+ return exit.value;
180
+ }
181
+
182
+ return throwCause(exit.cause);
183
+ };
184
+
185
+ /**
186
+ * Runs the embedded effect asynchronously and throws any failures and defects as errors.
187
+ * Inserts effect spans as stack frames.
188
+ * The error will have stack frames of where the effect was run (if stack trace limit allows).
189
+ * Removes effect runtime internal stack frames.
190
+ *
191
+ * To be used in place of `Effect.runPromise`.
192
+ *
193
+ * @throws AggregateError if there are multiple errors.
194
+ */
195
+ export const runAndForwardErrors = async <A, E>(
196
+ effect: Effect.Effect<A, E, never>,
197
+ options?: { signal?: AbortSignal },
198
+ ): Promise<A> => {
199
+ const exit = await Effect.runPromiseExit(effect, options);
200
+ return unwrapExit(exit);
201
+ };
202
+
203
+ /**
204
+ * Runs the embedded effect asynchronously and throws any failures and defects as errors.
205
+ */
206
+ export const runInRuntime: {
207
+ <R>(
208
+ runtime: Runtime.Runtime<R>,
209
+ ): <A, E>(effect: Effect.Effect<A, E, R>, options?: { signal?: AbortSignal } | undefined) => Promise<A>;
210
+ <R, A, E>(
211
+ runtime: Runtime.Runtime<R>,
212
+ effect: Effect.Effect<A, E, R>,
213
+ options?: { signal?: AbortSignal } | undefined,
214
+ ): Promise<A>;
215
+ } = (...args: any[]): any => {
216
+ if (args.length === 1) {
217
+ const [runtime] = args as [Runtime.Runtime<any>];
218
+ return async (
219
+ effect: Effect.Effect<any, any, any>,
220
+ options?: { signal?: AbortSignal } | undefined,
221
+ ): Promise<any> => {
222
+ const exit = await Runtime.runPromiseExit(runtime, effect, options);
223
+ return unwrapExit(exit);
224
+ };
225
+ } else {
226
+ const [runtime, effect, options] = args as [
227
+ Runtime.Runtime<any>,
228
+ Effect.Effect<any, any, any>,
229
+ { signal?: AbortSignal } | undefined,
230
+ ];
231
+ return (async () => {
232
+ const exit = await Runtime.runPromiseExit(runtime, effect, options);
233
+ return unwrapExit(exit);
234
+ })();
235
+ }
236
+ };
237
+
238
+ /**
239
+ * Like `Effect.promise` but also caputes spans for defects.
240
+ * Workaround for: https://github.com/Effect-TS/effect/issues/5436
241
+ */
242
+ export const promiseWithCauseCapture: <A>(evaluate: (signal: AbortSignal) => PromiseLike<A>) => Effect.Effect<A> = (
243
+ evaluate,
244
+ ) =>
245
+ Effect.promise(async (signal) => {
246
+ try {
247
+ const result = await evaluate(signal);
248
+ return Effect.succeed(result);
249
+ } catch (err) {
250
+ return Effect.die(err);
251
+ }
252
+ }).pipe(Effect.flatten);
package/src/index.ts CHANGED
@@ -3,5 +3,14 @@
3
3
  //
4
4
 
5
5
  export * from './ast';
6
- export * from './jsonPath';
6
+ export * from './atom-kvs';
7
+ export * from './context';
8
+ export * as DynamicRuntime from './dynamic-runtime';
9
+ export * from './errors';
10
+ export * from './json-path';
11
+ export * from './resource';
12
+ export * from './types';
7
13
  export * from './url';
14
+ export * as RuntimeProvider from './RuntimeProvider';
15
+ export * as Performance from './Performance';
16
+ export * from './async-task-tagging';
@@ -0,0 +1,35 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { it } from '@effect/vitest';
6
+ import * as Cause from 'effect/Cause';
7
+ import * as Effect from 'effect/Effect';
8
+ import * as Fiber from 'effect/Fiber';
9
+
10
+ import { runAndForwardErrors } from './errors';
11
+
12
+ const doWork = Effect.fn('doWork')(function* () {
13
+ yield* Effect.sleep('1 minute');
14
+ return 'work done';
15
+ });
16
+
17
+ it.effect.skip(
18
+ 'call a function to generate a research report',
19
+ Effect.fnUntraced(
20
+ function* (_) {
21
+ const resultFiber = yield* doWork().pipe(Effect.fork);
22
+ setTimeout(() => {
23
+ void runAndForwardErrors(Fiber.interrupt(resultFiber));
24
+ }, 2_000);
25
+
26
+ const result = yield* resultFiber;
27
+ console.log({ result });
28
+ },
29
+ Effect.catchAllCause((cause) => {
30
+ // console.log(inspect(cause, { depth: null, colors: true }));
31
+ console.log(Cause.pretty(cause));
32
+ return Effect.failCause(cause);
33
+ }),
34
+ ),
35
+ );
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { describe, expect, test } from 'vitest';
6
6
 
7
- import { createJsonPath, getField, isJsonPath, type JsonPath, splitJsonPath } from './jsonPath';
7
+ import { type JsonPath, createJsonPath, getField, getValue, isJsonPath, setValue, splitJsonPath } from './json-path';
8
8
 
9
9
  describe('createJsonPath', () => {
10
10
  test('supported path subset', () => {
@@ -32,9 +32,9 @@ describe('createJsonPath', () => {
32
32
 
33
33
  test('path splitting', () => {
34
34
  const cases = [
35
- ['foo.bar[0].baz', ['foo', 'bar', '0', 'baz']],
36
- ['users[1].name', ['users', '1', 'name']],
37
- ['data[0][1]', ['data', '0', '1']],
35
+ ['foo.bar[0].baz', ['foo', 'bar', 0, 'baz']],
36
+ ['users[1].name', ['users', 1, 'name']],
37
+ ['data[0][1]', ['data', 0, 1]],
38
38
  ['simple.path', ['simple', 'path']],
39
39
  ['root', ['root']],
40
40
  ] as const;
@@ -47,15 +47,15 @@ describe('createJsonPath', () => {
47
47
  test('path splitting - extended cases', () => {
48
48
  const cases = [
49
49
  // Multiple consecutive array indices.
50
- ['matrix[0][1][2]', ['matrix', '0', '1', '2']],
50
+ ['matrix[0][1][2]', ['matrix', 0, 1, 2]],
51
51
  // Properties with underscores and $.
52
52
  ['$_foo.bar_baz', ['$_foo', 'bar_baz']],
53
53
  // Deep nesting.
54
- ['very.deep.nested[0].property.path[5]', ['very', 'deep', 'nested', '0', 'property', 'path', '5']],
54
+ ['very.deep.nested[0].property.path[5]', ['very', 'deep', 'nested', 0, 'property', 'path', 5]],
55
55
  // Single character properties.
56
- ['a[0].b.c', ['a', '0', 'b', 'c']],
56
+ ['a[0].b.c', ['a', 0, 'b', 'c']],
57
57
  // Properties containing numbers.
58
- ['prop123.item456[7]', ['prop123', 'item456', '7']],
58
+ ['prop123.item456[7]', ['prop123', 'item456', 7]],
59
59
  ] as const;
60
60
 
61
61
  cases.forEach(([input, expected]) => {
@@ -99,3 +99,42 @@ describe('createJsonPath', () => {
99
99
  expect(getField({ a: 'foo' }, 'a' as JsonPath)).toBe('foo');
100
100
  });
101
101
  });
102
+
103
+ describe('Types', () => {
104
+ test('checks sanity', async ({ expect }) => {
105
+ const obj = {};
106
+ expect(obj).to.exist;
107
+ });
108
+ });
109
+
110
+ describe('get/set deep', () => {
111
+ test('get/set operations', ({ expect }) => {
112
+ const obj = {
113
+ name: 'test',
114
+ items: ['a', 'b', 'c'],
115
+ nested: {
116
+ prop: 'value',
117
+ arr: [1, 2, 3],
118
+ },
119
+ };
120
+
121
+ // Basic property access.
122
+ expect(getValue(obj, 'name' as JsonPath)).toBe('test');
123
+
124
+ // Array index access.
125
+ expect(getValue(obj, 'items[1]' as JsonPath)).toBe('b');
126
+
127
+ // Nested property access.
128
+ expect(getValue(obj, 'nested.prop' as JsonPath)).toBe('value');
129
+
130
+ // Nested array access.
131
+ expect(getValue(obj, 'nested.arr[2]' as JsonPath)).toBe(3);
132
+
133
+ // Setting values.
134
+ const updated1 = setValue(obj, 'items[1]' as JsonPath, 'x');
135
+ expect(updated1.items[1]).toBe('x');
136
+
137
+ const updated2 = setValue(obj, 'nested.arr[0]' as JsonPath, 99);
138
+ expect(updated2.nested.arr[0]).toBe(99);
139
+ });
140
+ });
@@ -2,20 +2,26 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { Schema, Option } from 'effect';
5
+ import * as Option from 'effect/Option';
6
+ import * as Schema from 'effect/Schema';
6
7
  import { JSONPath } from 'jsonpath-plus';
7
8
 
8
9
  import { invariant } from '@dxos/invariant';
10
+ import { getDeep, setDeep } from '@dxos/util';
9
11
 
10
12
  export type JsonProp = string & { __JsonPath: true; __JsonProp: true };
11
13
  export type JsonPath = string & { __JsonPath: true };
12
14
 
15
+ // TODO(burdon): Start with "$."?
16
+
13
17
  const PATH_REGEX = /^($|[a-zA-Z_$][\w$]*(?:\.[a-zA-Z_$][\w$]*|\[\d+\](?:\.)?)*$)/;
18
+
14
19
  const PROP_REGEX = /^\w+$/;
15
20
 
16
21
  /**
17
22
  * https://www.ietf.org/archive/id/draft-goessner-dispatch-jsonpath-00.html
18
23
  */
24
+ // TODO(burdon): Keys could be arbitrary strings.
19
25
  export const JsonPath = Schema.String.pipe(Schema.pattern(PATH_REGEX)).annotations({
20
26
  title: 'JSON path',
21
27
  description: 'JSON path to a property',
@@ -49,7 +55,7 @@ export const isJsonPath = (value: unknown): value is JsonPath => {
49
55
  * @param path Array of string or number segments
50
56
  * @returns Valid JsonPath or undefined if invalid
51
57
  */
52
- export const createJsonPath = (path: (string | number)[]): JsonPath => {
58
+ export const createJsonPath = (path: readonly (string | number)[]): JsonPath => {
53
59
  const candidatePath = path
54
60
  .map((p, i) => {
55
61
  if (typeof p === 'number') {
@@ -79,7 +85,7 @@ export const fromEffectValidationPath = (effectPath: string): JsonPath => {
79
85
  * Splits a JsonPath into its constituent parts.
80
86
  * Handles property access and array indexing.
81
87
  */
82
- export const splitJsonPath = (path: JsonPath): string[] => {
88
+ export const splitJsonPath = (path: JsonPath): (string | number)[] => {
83
89
  if (!isJsonPath(path)) {
84
90
  return [];
85
91
  }
@@ -87,14 +93,33 @@ export const splitJsonPath = (path: JsonPath): string[] => {
87
93
  return (
88
94
  path
89
95
  .match(/[a-zA-Z_$][\w$]*|\[\d+\]/g)
90
- ?.map((part) => (part.startsWith('[') ? part.replace(/[[\]]/g, '') : part)) ?? []
96
+ ?.map((part) => part.replace(/[[\]]/g, ''))
97
+ .map((part) => {
98
+ const parsed = Number.parseInt(part, 10);
99
+ return Number.isNaN(parsed) ? part : parsed;
100
+ }) ?? []
91
101
  );
92
102
  };
93
103
 
94
104
  /**
95
105
  * Applies a JsonPath to an object.
96
106
  */
107
+ // TODO(burdon): Reconcile with getValue.
97
108
  export const getField = (object: any, path: JsonPath): any => {
98
109
  // By default, JSONPath returns an array of results.
99
110
  return JSONPath({ path, json: object })[0];
100
111
  };
112
+
113
+ /**
114
+ * Get value from object using JsonPath.
115
+ */
116
+ export const getValue = <T extends object>(obj: T, path: JsonPath): any => {
117
+ return getDeep(obj, splitJsonPath(path));
118
+ };
119
+
120
+ /**
121
+ * Set value on object using JsonPath.
122
+ */
123
+ export const setValue = <T extends object>(obj: T, path: JsonPath, value: any): T => {
124
+ return setDeep(obj, splitJsonPath(path), value);
125
+ };