@dxos/functions 0.8.4-main.fffef41 → 0.9.0

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 (83) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +5 -7
  3. package/dist/lib/neutral/index.mjs +584 -0
  4. package/dist/lib/neutral/index.mjs.map +7 -0
  5. package/dist/lib/neutral/meta.json +1 -0
  6. package/dist/types/src/index.d.ts +0 -2
  7. package/dist/types/src/index.d.ts.map +1 -1
  8. package/dist/types/src/protocol/functions-ai-http-client.d.ts +12 -0
  9. package/dist/types/src/protocol/functions-ai-http-client.d.ts.map +1 -0
  10. package/dist/types/src/protocol/functions-ai-http-client.test.d.ts +2 -0
  11. package/dist/types/src/protocol/functions-ai-http-client.test.d.ts.map +1 -0
  12. package/dist/types/src/protocol/protocol.d.ts +14 -2
  13. package/dist/types/src/protocol/protocol.d.ts.map +1 -1
  14. package/dist/types/src/sdk.d.ts +5 -84
  15. package/dist/types/src/sdk.d.ts.map +1 -1
  16. package/dist/types/src/services/credentials.d.ts +17 -38
  17. package/dist/types/src/services/credentials.d.ts.map +1 -1
  18. package/dist/types/src/services/function-invocation-service.d.ts +10 -3
  19. package/dist/types/src/services/function-invocation-service.d.ts.map +1 -1
  20. package/dist/types/src/services/index.d.ts +1 -6
  21. package/dist/types/src/services/index.d.ts.map +1 -1
  22. package/dist/types/src/services/tracing.d.ts +1 -50
  23. package/dist/types/src/services/tracing.d.ts.map +1 -1
  24. package/dist/types/src/types/index.d.ts +0 -4
  25. package/dist/types/src/types/index.d.ts.map +1 -1
  26. package/dist/types/src/types/url.d.ts +6 -5
  27. package/dist/types/src/types/url.d.ts.map +1 -1
  28. package/dist/types/tsconfig.tsbuildinfo +1 -1
  29. package/package.json +25 -18
  30. package/src/index.ts +0 -2
  31. package/src/protocol/functions-ai-http-client.test.ts +105 -0
  32. package/src/protocol/functions-ai-http-client.ts +141 -0
  33. package/src/protocol/protocol.test.ts +10 -10
  34. package/src/protocol/protocol.ts +355 -86
  35. package/src/sdk.ts +12 -209
  36. package/src/services/credentials.ts +80 -108
  37. package/src/services/function-invocation-service.ts +20 -7
  38. package/src/services/index.ts +1 -7
  39. package/src/services/tracing.ts +0 -100
  40. package/src/types/index.ts +0 -4
  41. package/src/types/url.ts +6 -5
  42. package/dist/lib/browser/index.mjs +0 -927
  43. package/dist/lib/browser/index.mjs.map +0 -7
  44. package/dist/lib/browser/meta.json +0 -1
  45. package/dist/lib/node-esm/index.mjs +0 -928
  46. package/dist/lib/node-esm/index.mjs.map +0 -7
  47. package/dist/lib/node-esm/meta.json +0 -1
  48. package/dist/types/src/errors.d.ts +0 -129
  49. package/dist/types/src/errors.d.ts.map +0 -1
  50. package/dist/types/src/example/fib.d.ts +0 -7
  51. package/dist/types/src/example/fib.d.ts.map +0 -1
  52. package/dist/types/src/example/forex-effect.d.ts +0 -3
  53. package/dist/types/src/example/forex-effect.d.ts.map +0 -1
  54. package/dist/types/src/example/index.d.ts +0 -12
  55. package/dist/types/src/example/index.d.ts.map +0 -1
  56. package/dist/types/src/example/reply.d.ts +0 -3
  57. package/dist/types/src/example/reply.d.ts.map +0 -1
  58. package/dist/types/src/example/sleep.d.ts +0 -5
  59. package/dist/types/src/example/sleep.d.ts.map +0 -1
  60. package/dist/types/src/services/event-logger.d.ts +0 -87
  61. package/dist/types/src/services/event-logger.d.ts.map +0 -1
  62. package/dist/types/src/services/queues.d.ts +0 -47
  63. package/dist/types/src/services/queues.d.ts.map +0 -1
  64. package/dist/types/src/types/Function.d.ts +0 -58
  65. package/dist/types/src/types/Function.d.ts.map +0 -1
  66. package/dist/types/src/types/Script.d.ts +0 -28
  67. package/dist/types/src/types/Script.d.ts.map +0 -1
  68. package/dist/types/src/types/Trigger.d.ts +0 -139
  69. package/dist/types/src/types/Trigger.d.ts.map +0 -1
  70. package/dist/types/src/types/TriggerEvent.d.ts +0 -44
  71. package/dist/types/src/types/TriggerEvent.d.ts.map +0 -1
  72. package/src/errors.ts +0 -21
  73. package/src/example/fib.ts +0 -32
  74. package/src/example/forex-effect.ts +0 -40
  75. package/src/example/index.ts +0 -13
  76. package/src/example/reply.ts +0 -21
  77. package/src/example/sleep.ts +0 -24
  78. package/src/services/event-logger.ts +0 -127
  79. package/src/services/queues.ts +0 -84
  80. package/src/types/Function.ts +0 -62
  81. package/src/types/Script.ts +0 -33
  82. package/src/types/Trigger.ts +0 -139
  83. package/src/types/TriggerEvent.ts +0 -62
@@ -2,44 +2,79 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
+ import * as AnthropicClient from '@effect/ai-anthropic/AnthropicClient';
5
6
  import * as Effect from 'effect/Effect';
6
7
  import * as Layer from 'effect/Layer';
7
8
  import * as Schema from 'effect/Schema';
8
9
  import * as SchemaAST from 'effect/SchemaAST';
9
10
 
10
- import { AiService } from '@dxos/ai';
11
- import { Type } from '@dxos/echo';
12
- import { EchoClient } from '@dxos/echo-db';
13
- import { acquireReleaseResource } from '@dxos/effect';
14
- import { failedInvariant, invariant } from '@dxos/invariant';
11
+ import { AiModelResolver, AiService, OpaqueToolkit } from '@dxos/ai';
12
+ import { AnthropicResolver } from '@dxos/ai/resolvers';
13
+ import {
14
+ FunctionError,
15
+ Header,
16
+ InvalidOperationInputError,
17
+ InvalidOperationOutputError,
18
+ Operation,
19
+ Trace,
20
+ } from '@dxos/compute';
21
+ import { LifecycleState, Resource } from '@dxos/context';
22
+ import { Database, Feed, JsonSchema, Ref, Registry, type Type } from '@dxos/echo';
23
+ import {
24
+ createFeedServiceLayer,
25
+ EchoClient,
26
+ type DatabaseImpl,
27
+ makeRegistry,
28
+ type QueueFactory,
29
+ } from '@dxos/echo-client';
30
+ import { refFromEncodedReference } from '@dxos/echo/internal';
31
+ import { EffectEx } from '@dxos/effect';
32
+ import { assertState, failedInvariant, invariant } from '@dxos/invariant';
15
33
  import { PublicKey } from '@dxos/keys';
16
- import { type FunctionProtocol } from '@dxos/protocols';
34
+ import { log } from '@dxos/log';
35
+ import { EdgeFunctionEnv, ErrorCodec, type FunctionProtocol, type TraceProtocol } from '@dxos/protocols';
17
36
 
18
- import { FunctionError } from '../errors';
19
- import { FunctionDefinition, type FunctionServices } from '../sdk';
20
- import { CredentialsService, DatabaseService, FunctionInvocationService, TracingService } from '../services';
21
- import { QueueService } from '../services';
37
+ import { type FunctionServices } from '../sdk';
38
+ import { configuredCredentialsLayer, credentialsLayerFromDatabase, FunctionInvocationService } from '../services';
39
+ import { FunctionsAiHttpClient } from './functions-ai-http-client';
40
+
41
+ export interface FunctionWrappingOptions {
42
+ /**
43
+ * Additional types to register with the database.
44
+ */
45
+ types?: Type.AnyEntity[];
46
+
47
+ /**
48
+ * Toolkits to make available via the `OpaqueToolkitProvider`.
49
+ */
50
+ toolkits?: OpaqueToolkit.OpaqueToolkit[];
51
+ }
22
52
 
23
53
  /**
24
54
  * Wraps a function handler made with `defineFunction` to a protocol that the functions-runtime expects.
25
55
  */
26
- export const wrapFunctionHandler = (func: FunctionDefinition): FunctionProtocol.Func => {
27
- if (!FunctionDefinition.isFunction(func)) {
28
- throw new TypeError('Invalid function definition');
56
+ export const wrapFunctionHandler = (
57
+ func: Operation.WithHandler<Operation.Definition.Any>,
58
+ opts: FunctionWrappingOptions = {},
59
+ ): FunctionProtocol.Func => {
60
+ if (!Operation.isOperationWithHandler(func)) {
61
+ throw new TypeError('Expected operation with handler');
29
62
  }
30
63
 
64
+ const serviceTags = func.services.map((service) => service.key);
65
+
31
66
  return {
32
67
  meta: {
33
- key: func.key,
34
- name: func.name,
35
- description: func.description,
36
- inputSchema: Type.toJsonSchema(func.inputSchema),
37
- outputSchema: func.outputSchema === undefined ? undefined : Type.toJsonSchema(func.outputSchema),
38
- services: func.services,
68
+ key: func.meta.key,
69
+ name: func.meta.name,
70
+ description: func.meta.description,
71
+ inputSchema: JsonSchema.toJsonSchema(func.input),
72
+ outputSchema: func.output === undefined ? undefined : JsonSchema.toJsonSchema(func.output),
73
+ services: func.services.map((service) => service.key),
39
74
  },
40
75
  handler: async ({ data, context }) => {
41
76
  if (
42
- (func.services.includes(DatabaseService.key) || func.services.includes(QueueService.key)) &&
77
+ (serviceTags.includes(Database.Service.key) || serviceTags.includes(Feed.FeedService.key)) &&
43
78
  (!context.services.dataService || !context.services.queryService)
44
79
  ) {
45
80
  throw new FunctionError({
@@ -47,99 +82,333 @@ export const wrapFunctionHandler = (func: FunctionDefinition): FunctionProtocol.
47
82
  });
48
83
  }
49
84
 
85
+ // eslint-disable-next-line no-useless-catch
50
86
  try {
51
- if (!SchemaAST.isAnyKeyword(func.inputSchema.ast)) {
52
- Schema.validateSync(func.inputSchema)(data);
87
+ if (!SchemaAST.isAnyKeyword(func.input.ast)) {
88
+ try {
89
+ Schema.validateSync(func.input, { onExcessProperty: 'error' })(data);
90
+ } catch (error: any) {
91
+ throw new InvalidOperationInputError({
92
+ message: `Operation input did not match schema (${func.meta.key}): ${error.message}`,
93
+ cause: error,
94
+ });
95
+ }
53
96
  }
54
97
 
55
- let result = await func.handler({
56
- // TODO(dmaretskyi): Fix the types.
57
- context: context as any,
58
- data,
59
- });
98
+ await using funcContext = await new FunctionContext(context, opts).open();
99
+
100
+ const types = [...(opts.types ?? []), ...(func.types ?? [])];
101
+ if (types.length > 0) {
102
+ invariant(funcContext.db, 'Database is required for functions with types');
103
+ funcContext.db.graph.registry.add(types);
104
+ }
105
+
106
+ const dataWithDecodedRefs =
107
+ funcContext.db && !SchemaAST.isAnyKeyword(func.input.ast)
108
+ ? decodeRefsFromSchema(func.input.ast, data, funcContext.db)
109
+ : data;
110
+
111
+ let result: any = await func.handler(dataWithDecodedRefs);
60
112
 
61
113
  if (Effect.isEffect(result)) {
62
- result = await Effect.runPromise(
114
+ result = await EffectEx.runAndForwardErrors(
63
115
  (result as Effect.Effect<unknown, unknown, FunctionServices>).pipe(
64
116
  Effect.orDie,
65
- Effect.provide(createServiceLayer(context)),
117
+ Effect.provide(funcContext.createLayer()),
66
118
  ),
67
119
  );
68
120
  }
69
121
 
70
- if (func.outputSchema && !SchemaAST.isAnyKeyword(func.outputSchema.ast)) {
71
- Schema.validateSync(func.outputSchema)(result);
122
+ // Flush in-memory ECHO writes before the function scope closes.
123
+ // Writes performed by `db.add` / `db.remove` are buffered in the in-memory
124
+ // `DatabaseImpl` and only pushed across the `DataService` binding when
125
+ // `db.flush({ disk })` is called. `FunctionContext._close` (invoked by the
126
+ // `await using` above) calls `db.close()` but does NOT flush, so mutations
127
+ // performed by handlers that declare `Database.Service` (e.g. `object-create`,
128
+ // `object-update`, `relation-create`) would be silently dropped before reaching
129
+ // the edge `AutomergeReplicator`. Flushing here closes that hole.
130
+ if (serviceTags.includes(Database.Service.key) && funcContext.db) {
131
+ await funcContext.db.flush({ disk: true, indexes: false });
132
+ }
133
+
134
+ if (func.output && !SchemaAST.isAnyKeyword(func.output.ast)) {
135
+ try {
136
+ Schema.validateSync(func.output, { onExcessProperty: 'error' })(result);
137
+ } catch (error: any) {
138
+ throw new InvalidOperationOutputError({
139
+ message: `Operation output did not match schema (${func.meta.key}): ${error.message}`,
140
+ cause: error,
141
+ });
142
+ }
72
143
  }
73
144
 
74
145
  return result;
75
146
  } catch (error) {
76
- if (FunctionError.is(error)) {
77
- throw error;
78
- } else {
79
- throw new FunctionError({
80
- cause: error,
81
- context: { func: func.key },
82
- });
83
- }
147
+ // TODO(dmaretskyi): We might do error wrapping here and add extra context.
148
+ throw error;
84
149
  }
85
150
  },
86
151
  };
87
152
  };
88
153
 
89
154
  /**
90
- * Creates a layer of services for the function.
155
+ * Container for services and context for a function.
156
+ */
157
+ class FunctionContext extends Resource {
158
+ readonly context: FunctionProtocol.Context;
159
+ readonly client: EchoClient | undefined;
160
+ db: DatabaseImpl | undefined;
161
+ queues: QueueFactory | undefined;
162
+ readonly opts: FunctionWrappingOptions;
163
+
164
+ constructor(context: FunctionProtocol.Context, opts: FunctionWrappingOptions) {
165
+ super();
166
+ this.context = context;
167
+ this.opts = opts;
168
+ if (context.services.dataService && context.services.queryService) {
169
+ this.client = new EchoClient().connectToService({
170
+ dataService: context.services.dataService,
171
+ queryService: context.services.queryService,
172
+ queueService: context.services.queueService,
173
+ });
174
+ }
175
+ }
176
+
177
+ override async _open() {
178
+ await this.client?.open();
179
+ this.db =
180
+ this.client && this.context.spaceId
181
+ ? this.client.constructDatabase({
182
+ spaceId: this.context.spaceId ?? failedInvariant(),
183
+ spaceKey: PublicKey.fromHex(this.context.spaceKey ?? failedInvariant('spaceKey missing in context')),
184
+ reactiveSchemaQuery: false,
185
+ preloadSchemaOnOpen: false,
186
+ })
187
+ : undefined;
188
+
189
+ await this.db?.setSpaceRoot(this.context.spaceRootUrl ?? failedInvariant('spaceRootUrl missing in context'));
190
+ await this.db?.open();
191
+ this.queues =
192
+ this.client && this.context.spaceId ? this.client.constructQueueFactory(this.context.spaceId) : undefined;
193
+ }
194
+
195
+ override async _close() {
196
+ await this.db?.close();
197
+ await this.client?.close();
198
+ }
199
+
200
+ createLayer(): Layer.Layer<FunctionServices> {
201
+ assertState(this._lifecycleState === LifecycleState.OPEN, 'FunctionContext is not open');
202
+
203
+ const dbLayer = this.db ? Database.layer(this.db) : Database.notAvailable;
204
+ const feedLayer = this.queues ? createFeedServiceLayer(this.queues) : Feed.notAvailable;
205
+ const credentials = dbLayer
206
+ ? credentialsLayerFromDatabase({ caching: true }).pipe(Layer.provide(dbLayer))
207
+ : configuredCredentialsLayer([]);
208
+
209
+ const aiLayer = this.context.services.functionsAiService
210
+ ? InternalAiServiceLayer(this.context.services.functionsAiService).pipe(Layer.provide(credentials))
211
+ : AiService.notAvailable;
212
+
213
+ const operationServiceLayer = this.context.services.functionsService
214
+ ? makeOperationServiceLayer(this.context.services.functionsService)
215
+ : unavailableOperationServiceLayer;
216
+
217
+ const traceWriterLayer = this.context.services.traceService
218
+ ? makeTraceWriterLayer(this.context.services.traceService)
219
+ : Trace.writerLayerNoop;
220
+
221
+ log('Creating function context layer', {
222
+ traceService: !!this.context.services.traceService,
223
+ functionsService: !!this.context.services.functionsService,
224
+ functionsAiService: !!this.context.services.functionsAiService,
225
+ spaceId: this.context.spaceId,
226
+ spaceRootUrl: this.context.spaceRootUrl,
227
+ toolkits: this.opts.toolkits?.length ?? 0,
228
+ types: this.opts.types?.length ?? 0,
229
+ });
230
+
231
+ const registryLayer = this.db
232
+ ? Layer.succeed(Registry.Service, this.db.graph.registry)
233
+ : Layer.succeed(Registry.Service, makeRegistry());
234
+
235
+ return Layer.mergeAll(
236
+ dbLayer,
237
+ feedLayer,
238
+ credentials,
239
+ operationServiceLayer,
240
+ aiLayer,
241
+ OpaqueToolkit.providerLayer(OpaqueToolkit.merge(...(this.opts.toolkits ?? []))),
242
+ traceWriterLayer,
243
+ registryLayer,
244
+
245
+ // `FunctionInvocationService` is deprecated; new code should yield `Operation.Service`.
246
+ // The cloudflare wrapper provides only the unavailable layer to satisfy the (still-present)
247
+ // type union — handlers that yield it will die at invocation time.
248
+ FunctionInvocationService.layerNotAvailable,
249
+ );
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Backs `Trace.TraceService` with the EDGE-provided `TraceService` so that operation
255
+ * handlers can write trace events that are forwarded to the runtime's trace sink.
91
256
  */
92
- const createServiceLayer = (context: FunctionProtocol.Context): Layer.Layer<FunctionServices> => {
93
- return Layer.unwrapScoped(
94
- Effect.gen(function* () {
95
- let client: EchoClient | undefined;
96
-
97
- if (context.services.dataService && context.services.queryService) {
98
- client = yield* acquireReleaseResource(() => {
99
- invariant(context.services.dataService && context.services.queryService);
100
- // TODO(dmaretskyi): Queues service.
101
- return new EchoClient().connectToService({
102
- dataService: context.services.dataService,
103
- queryService: context.services.queryService,
104
- queueService: context.services.queueService,
105
- });
257
+ const makeTraceWriterLayer = (traceService: TraceProtocol.TraceService): Layer.Layer<Trace.TraceService> =>
258
+ Layer.succeed(Trace.TraceService, {
259
+ write: (eventType, payload) => {
260
+ log('Writing trace event', {
261
+ eventType: eventType.key,
262
+ });
263
+ traceService.write([
264
+ {
265
+ key: eventType.key,
266
+ isEphemeral: eventType.isEphemeral,
267
+ data: payload,
268
+ },
269
+ ]);
270
+ },
271
+ });
272
+
273
+ /** Proxies Anthropic requests through the EDGE-provided `FunctionsAiService`, BYOK-wrapped. */
274
+ const InternalAiServiceLayer = (functionsAiService: EdgeFunctionEnv.FunctionsAiService) => {
275
+ // `apiUrl` is a sentinel — the request gets re-routed by the AI gateway in EDGE.
276
+ const httpClient = Header.byokLayer('anthropic.com').pipe(
277
+ Layer.provide(FunctionsAiHttpClient.layer(functionsAiService)),
278
+ );
279
+ const anthropicClient = AnthropicClient.layer({ apiUrl: 'http://internal/provider/anthropic' }).pipe(
280
+ Layer.provide(httpClient),
281
+ );
282
+ const resolver = AnthropicResolver.make().pipe(Layer.provide(anthropicClient));
283
+ return AiModelResolver.AiModelResolver.buildAiService.pipe(Layer.provide(resolver));
284
+ };
285
+
286
+ /**
287
+ * Backs `Operation.Service` with the EDGE-provided `FunctionsService` so that operation
288
+ * handlers can invoke other deployed operations remotely. The `deployedId` on the operation
289
+ * definition is used as the routing key.
290
+ */
291
+ const makeOperationServiceLayer = (
292
+ functionsService: EdgeFunctionEnv.FunctionsService,
293
+ ): Layer.Layer<Operation.Service> => {
294
+ const invokeRemote = async (
295
+ op: Operation.Definition.Any,
296
+ input: unknown,
297
+ options?: Operation.InvokeOptions,
298
+ ): Promise<{ data?: unknown; error?: Error }> => {
299
+ invariant(op.meta.deployedId, `Operation '${op.meta.key}' has no deployedId; cannot invoke remotely.`);
300
+ const result = await functionsService.invoke(op.meta.deployedId, input, {
301
+ spaceId: options?.spaceId,
302
+ // Forward the conversation DXN so the remote runtime can rebuild conversation-scoped
303
+ // services (e.g. `AiContext.Service`) needed by operations like `GetContext`.
304
+ conversation: options?.conversation,
305
+ });
306
+ if (result._kind === 'success') {
307
+ return { data: result.data };
308
+ }
309
+ return { error: ErrorCodec.decode(result.error) };
310
+ };
311
+
312
+ return Layer.succeed(Operation.Service, {
313
+ invoke: ((op: Operation.Definition.Any, input: unknown, options?: Operation.InvokeOptions) =>
314
+ Effect.tryPromise(() => invokeRemote(op, input, options)).pipe(
315
+ Effect.orDie,
316
+ Effect.flatMap((outcome) =>
317
+ outcome.error ? Effect.die(outcome.error) : Effect.succeed(outcome.data as never),
318
+ ),
319
+ )) as Operation.OperationService['invoke'],
320
+ schedule: ((op: Operation.Definition.Any, input: unknown, _options?: Operation.InvokeOptions) =>
321
+ Effect.sync(() => {
322
+ invariant(op.meta.deployedId, `Operation '${op.meta.key}' has no deployedId; cannot schedule remotely.`);
323
+ // Fire and forget — schedule is intentionally non-awaiting.
324
+ void functionsService.invoke(op.meta.deployedId, input).catch(() => {
325
+ // Swallow errors — schedule is observability-only.
106
326
  });
327
+ })) as Operation.OperationService['schedule'],
328
+ invokePromise: ((op: Operation.Definition.Any, input: unknown, options?: Operation.InvokeOptions) =>
329
+ invokeRemote(op, input, options).catch((error: unknown) => ({
330
+ error: error instanceof Error ? error : new Error(String(error)),
331
+ }))) as Operation.OperationService['invokePromise'],
332
+ } satisfies Operation.OperationService);
333
+ };
334
+
335
+ const unavailableOperationServiceLayer = Layer.succeed(Operation.Service, {
336
+ invoke: () => Effect.die('Operation.Service is not available: missing functionsService in EDGE context.'),
337
+ schedule: () => Effect.die('Operation.Service is not available: missing functionsService in EDGE context.'),
338
+ invokePromise: async () => ({
339
+ error: new Error('Operation.Service is not available: missing functionsService in EDGE context.'),
340
+ }),
341
+ } as Operation.OperationService);
342
+
343
+ const decodeRefsFromSchema = (ast: SchemaAST.AST, value: unknown, db: DatabaseImpl): unknown => {
344
+ if (value == null) {
345
+ return value;
346
+ }
347
+
348
+ const encoded = SchemaAST.encodedBoundAST(ast);
349
+ if (Ref.isRefType(encoded)) {
350
+ if (Ref.isRef(value)) {
351
+ return value;
352
+ }
353
+
354
+ if (typeof value === 'object' && value !== null && typeof (value as any)['/'] === 'string') {
355
+ const resolver = db.graph.createRefResolver({ context: { space: db.spaceId } });
356
+ return refFromEncodedReference(value as any, resolver);
357
+ }
358
+
359
+ return value;
360
+ }
361
+
362
+ switch (encoded._tag) {
363
+ case 'TypeLiteral': {
364
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
365
+ return value;
107
366
  }
367
+ const result: Record<string, unknown> = { ...(value as any) };
368
+ for (const prop of SchemaAST.getPropertySignatures(encoded)) {
369
+ const key = prop.name.toString();
370
+ if (key in result) {
371
+ result[key] = decodeRefsFromSchema(prop.type, (result as any)[key], db);
372
+ }
373
+ }
374
+ return result;
375
+ }
108
376
 
109
- const db =
110
- client && context.spaceId
111
- ? yield* acquireReleaseResource(() =>
112
- client.constructDatabase({
113
- spaceId: context.spaceId ?? failedInvariant(),
114
- spaceKey: PublicKey.fromHex(context.spaceKey ?? failedInvariant('spaceKey missing in context')),
115
- reactiveSchemaQuery: false,
116
- }),
117
- )
118
- : undefined;
119
-
120
- if (db) {
121
- console.log('Setting space root', context.spaceRootUrl);
122
- yield* Effect.promise(() =>
123
- db!.setSpaceRoot(context.spaceRootUrl ?? failedInvariant('spaceRootUrl missing in context')),
124
- );
377
+ case 'TupleType': {
378
+ if (!Array.isArray(value)) {
379
+ return value;
125
380
  }
126
381
 
127
- const queues = client && context.spaceId ? client.constructQueueFactory(context.spaceId) : undefined;
382
+ // For arrays, effect uses TupleType with empty elements and a single rest element.
383
+ if (encoded.elements.length === 0 && encoded.rest.length === 1) {
384
+ const elementType = encoded.rest[0].type;
385
+ return (value as unknown[]).map((item) => decodeRefsFromSchema(elementType, item, db));
386
+ }
128
387
 
129
- const dbLayer = db ? DatabaseService.layer(db) : DatabaseService.notAvailable;
130
- const queuesLayer = queues ? QueueService.layer(queues) : QueueService.notAvailable;
131
- const credentials = dbLayer
132
- ? CredentialsService.layerFromDatabase().pipe(Layer.provide(dbLayer))
133
- : CredentialsService.configuredLayer([]);
134
- const functionInvocationService = MockedFunctionInvocationService;
135
- const aiService = AiService.notAvailable;
136
- const tracing = TracingService.layerNoop;
388
+ return value;
389
+ }
137
390
 
138
- return Layer.mergeAll(dbLayer, queuesLayer, credentials, functionInvocationService, aiService, tracing);
139
- }),
140
- );
141
- };
391
+ case 'Union': {
392
+ // Optional values are represented as union with undefined.
393
+ const nonUndefined = encoded.types.filter((t) => !SchemaAST.isUndefinedKeyword(t));
394
+ if (nonUndefined.length === 1) {
395
+ return decodeRefsFromSchema(nonUndefined[0], value, db);
396
+ }
397
+
398
+ // For other unions we can't safely pick a branch without validating.
399
+ return value;
400
+ }
401
+
402
+ case 'Suspend': {
403
+ return decodeRefsFromSchema(encoded.f(), value, db);
404
+ }
142
405
 
143
- const MockedFunctionInvocationService = Layer.succeed(FunctionInvocationService, {
144
- invokeFunction: () => Effect.die('Calling functions from functions is not implemented yet.'),
145
- });
406
+ case 'Refinement': {
407
+ return decodeRefsFromSchema(encoded.from, value, db);
408
+ }
409
+
410
+ default: {
411
+ return value;
412
+ }
413
+ }
414
+ };