@dxos/effect 0.8.4-main.b97322e → 0.8.4-main.bc2380dfbc

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 (86) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +1 -1
  3. package/dist/lib/browser/chunk-CGS2ULMK.mjs +11 -0
  4. package/dist/lib/browser/chunk-CGS2ULMK.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +519 -293
  6. package/dist/lib/browser/index.mjs.map +4 -4
  7. package/dist/lib/browser/meta.json +1 -1
  8. package/dist/lib/browser/testing.mjs +31 -0
  9. package/dist/lib/browser/testing.mjs.map +7 -0
  10. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +11 -0
  11. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +7 -0
  12. package/dist/lib/node-esm/index.mjs +519 -293
  13. package/dist/lib/node-esm/index.mjs.map +4 -4
  14. package/dist/lib/node-esm/meta.json +1 -1
  15. package/dist/lib/node-esm/testing.mjs +31 -0
  16. package/dist/lib/node-esm/testing.mjs.map +7 -0
  17. package/dist/types/src/Performance.d.ts +25 -0
  18. package/dist/types/src/Performance.d.ts.map +1 -0
  19. package/dist/types/src/RuntimeProvider.d.ts +21 -0
  20. package/dist/types/src/RuntimeProvider.d.ts.map +1 -0
  21. package/dist/types/src/ast.d.ts +42 -22
  22. package/dist/types/src/ast.d.ts.map +1 -1
  23. package/dist/types/src/async-task-tagging.d.ts +6 -0
  24. package/dist/types/src/async-task-tagging.d.ts.map +1 -0
  25. package/dist/types/src/atom-kvs.d.ts +19 -0
  26. package/dist/types/src/atom-kvs.d.ts.map +1 -0
  27. package/dist/types/src/context.d.ts +2 -1
  28. package/dist/types/src/context.d.ts.map +1 -1
  29. package/dist/types/src/dynamic-runtime.d.ts +56 -0
  30. package/dist/types/src/dynamic-runtime.d.ts.map +1 -0
  31. package/dist/types/src/dynamic-runtime.test.d.ts +2 -0
  32. package/dist/types/src/dynamic-runtime.test.d.ts.map +1 -0
  33. package/dist/types/src/errors.d.ts +43 -1
  34. package/dist/types/src/errors.d.ts.map +1 -1
  35. package/dist/types/src/index.d.ts +8 -3
  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 +6 -2
  50. package/dist/types/src/resource.d.ts.map +1 -1
  51. package/dist/types/src/testing.d.ts +26 -3
  52. package/dist/types/src/testing.d.ts.map +1 -1
  53. package/dist/types/src/types.d.ts +8 -0
  54. package/dist/types/src/types.d.ts.map +1 -0
  55. package/dist/types/src/url.d.ts +3 -1
  56. package/dist/types/src/url.d.ts.map +1 -1
  57. package/dist/types/tsconfig.tsbuildinfo +1 -1
  58. package/package.json +34 -13
  59. package/src/Performance.ts +45 -0
  60. package/src/RuntimeProvider.ts +35 -0
  61. package/src/ast.test.ts +39 -11
  62. package/src/ast.ts +149 -96
  63. package/src/async-task-tagging.ts +51 -0
  64. package/src/atom-kvs.ts +35 -0
  65. package/src/context.ts +2 -1
  66. package/src/dynamic-runtime.test.ts +465 -0
  67. package/src/dynamic-runtime.ts +195 -0
  68. package/src/errors.test.ts +1 -1
  69. package/src/errors.ts +142 -28
  70. package/src/index.ts +8 -3
  71. package/src/interrupt.test.ts +35 -0
  72. package/src/{jsonPath.test.ts → json-path.test.ts} +47 -8
  73. package/src/{jsonPath.ts → json-path.ts} +29 -4
  74. package/src/layers.test.ts +112 -0
  75. package/src/otel.test.ts +126 -0
  76. package/src/otel.ts +45 -0
  77. package/src/resource.test.ts +5 -4
  78. package/src/resource.ts +10 -5
  79. package/src/sanity.test.ts +30 -15
  80. package/src/testing.ts +31 -3
  81. package/src/types.ts +11 -0
  82. package/src/url.test.ts +1 -1
  83. package/src/url.ts +5 -2
  84. package/dist/types/src/jsonPath.d.ts.map +0 -1
  85. package/dist/types/src/jsonPath.test.d.ts +0 -2
  86. 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
+ }
@@ -2,7 +2,7 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { Data } from 'effect';
5
+ import * as Data from 'effect/Data';
6
6
  import { test } from 'vitest';
7
7
 
8
8
  class MyError extends Data.TaggedError('MyError')<{
package/src/errors.ts CHANGED
@@ -2,11 +2,16 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { Cause, Chunk, Effect, Exit, GlobalValue, Option } from 'effect';
6
- import type { AnySpan, Span } from 'effect/Tracer';
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';
7
13
 
8
14
  const spanSymbol = Symbol.for('effect/SpanAnnotation');
9
- const originalSymbol = Symbol.for('effect/OriginalAnnotation');
10
15
  const spanToTrace = GlobalValue.globalValue('effect/Tracer/spanToTrace', () => new WeakMap());
11
16
  const locationRegex = /\((.*)\)/g;
12
17
 
@@ -16,12 +21,19 @@ const locationRegex = /\((.*)\)/g;
16
21
  * Unwraps error proxy.
17
22
  */
18
23
  const prettyErrorStack = (error: any, appendStacks: string[] = []): any => {
24
+ if (typeof error !== 'object' || error === null) {
25
+ return error;
26
+ }
27
+
19
28
  const span = error[spanSymbol];
20
29
 
21
30
  const lines = typeof error.stack === 'string' ? error.stack.split('\n') : [];
22
31
  const out = [];
23
32
 
24
- let atStack = false;
33
+ // Very hacky way to remove effect runtime internal stack frames.
34
+ let atStack = false,
35
+ inCore = false,
36
+ passedScheduler = false;
25
37
  for (let i = 0; i < lines.length; i++) {
26
38
  if (!atStack && !lines[i].startsWith(' at ')) {
27
39
  out.push(lines[i]);
@@ -39,6 +51,26 @@ const prettyErrorStack = (error: any, appendStacks: string[] = []): any => {
39
51
  if (lines[i].includes('effect_internal_function')) {
40
52
  break;
41
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
+
42
74
  out.push(
43
75
  lines[i]
44
76
  .replace(/at .*effect_instruction_i.*\((.*)\)/, 'at $1')
@@ -48,7 +80,7 @@ const prettyErrorStack = (error: any, appendStacks: string[] = []): any => {
48
80
  }
49
81
 
50
82
  if (span) {
51
- let current: Span | AnySpan | undefined = span;
83
+ let current: Tracer.Span | Tracer.AnySpan | undefined = span;
52
84
  let i = 0;
53
85
  while (current && current._tag === 'Span' && i < 10) {
54
86
  const stackFn = spanToTrace.get(current);
@@ -77,9 +109,7 @@ const prettyErrorStack = (error: any, appendStacks: string[] = []): any => {
77
109
 
78
110
  out.push(...appendStacks);
79
111
 
80
- if (error[originalSymbol]) {
81
- error = error[originalSymbol];
82
- }
112
+ error = Cause.originalError(error);
83
113
  if (error.cause) {
84
114
  error.cause = prettyErrorStack(error.cause);
85
115
  }
@@ -95,7 +125,7 @@ const prettyErrorStack = (error: any, appendStacks: string[] = []): any => {
95
125
  };
96
126
 
97
127
  /**
98
- * Runs the embedded effect asynchronously and throws any failures and defects as errors.
128
+ * Converts a cause to an error.
99
129
  * Inserts effect spans as stack frames.
100
130
  * The error will have stack frames of where the effect was run (if stack trace limit allows).
101
131
  * Removes effect runtime internal stack frames.
@@ -104,35 +134,119 @@ const prettyErrorStack = (error: any, appendStacks: string[] = []): any => {
104
134
  *
105
135
  * @throws AggregateError if there are multiple errors.
106
136
  */
107
- export const runAndForwardErrors = async <A, E>(
108
- effect: Effect.Effect<A, E, never>,
109
- options?: { signal?: AbortSignal },
110
- ): Promise<A> => {
111
- const exit = await Effect.runPromiseExit(effect, options);
112
- if (Exit.isSuccess(exit)) {
113
- return exit.value;
114
- }
115
-
116
- if (Cause.isEmpty(exit.cause)) {
117
- throw new Error('Fiber failed without a cause');
118
- } else if (Cause.isInterrupted(exit.cause)) {
119
- throw new Error('Fiber was interrupted');
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');
120
142
  } else {
121
- const errors = [...Chunk.toArray(Cause.failures(exit.cause)), ...Chunk.toArray(Cause.defects(exit.cause))];
143
+ const errors = [...Chunk.toArray(Cause.failures(cause)), ...Chunk.toArray(Cause.defects(cause))];
122
144
 
123
145
  const getStackFrames = (): string[] => {
124
- const o: { stack: string } = {} as any;
125
- Error.captureStackTrace(o, getStackFrames);
126
- return o.stack.split('\n').slice(1);
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);
127
150
  };
128
151
 
129
152
  const stackFrames = getStackFrames();
130
153
  const newErrors = errors.map((error) => prettyErrorStack(error, stackFrames));
131
154
 
132
155
  if (newErrors.length === 1) {
133
- throw newErrors[0];
156
+ return newErrors[0];
134
157
  } else {
135
- throw new AggregateError(newErrors);
158
+ return new AggregateError(newErrors);
136
159
  }
137
160
  }
138
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,9 +3,14 @@
3
3
  //
4
4
 
5
5
  export * from './ast';
6
- export * from './jsonPath';
7
- export * from './url';
6
+ export * from './atom-kvs';
8
7
  export * from './context';
8
+ export * as DynamicRuntime from './dynamic-runtime';
9
9
  export * from './errors';
10
- export * from './testing';
10
+ export * from './json-path';
11
11
  export * from './resource';
12
+ export * from './types';
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
+ };