@dxos/functions 0.8.4-main.e8ec1fe → 0.8.4-main.ef1bc66f44

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 (57) hide show
  1. package/dist/lib/{browser → neutral}/index.mjs +450 -148
  2. package/dist/lib/neutral/index.mjs.map +7 -0
  3. package/dist/lib/neutral/meta.json +1 -0
  4. package/dist/types/src/errors.d.ts +24 -32
  5. package/dist/types/src/errors.d.ts.map +1 -1
  6. package/dist/types/src/operation-compatibility.test.d.ts +2 -0
  7. package/dist/types/src/operation-compatibility.test.d.ts.map +1 -0
  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/protocol.d.ts.map +1 -1
  11. package/dist/types/src/sdk.d.ts +27 -2
  12. package/dist/types/src/sdk.d.ts.map +1 -1
  13. package/dist/types/src/services/credentials.d.ts +6 -4
  14. package/dist/types/src/services/credentials.d.ts.map +1 -1
  15. package/dist/types/src/services/event-logger.d.ts +25 -31
  16. package/dist/types/src/services/event-logger.d.ts.map +1 -1
  17. package/dist/types/src/services/function-invocation-service.d.ts +5 -0
  18. package/dist/types/src/services/function-invocation-service.d.ts.map +1 -1
  19. package/dist/types/src/services/index.d.ts +0 -1
  20. package/dist/types/src/services/index.d.ts.map +1 -1
  21. package/dist/types/src/services/queues.d.ts +4 -4
  22. package/dist/types/src/services/queues.d.ts.map +1 -1
  23. package/dist/types/src/services/tracing.d.ts +37 -3
  24. package/dist/types/src/services/tracing.d.ts.map +1 -1
  25. package/dist/types/src/types/Function.d.ts +40 -46
  26. package/dist/types/src/types/Function.d.ts.map +1 -1
  27. package/dist/types/src/types/Script.d.ts +9 -16
  28. package/dist/types/src/types/Script.d.ts.map +1 -1
  29. package/dist/types/src/types/Trigger.d.ts +58 -76
  30. package/dist/types/src/types/Trigger.d.ts.map +1 -1
  31. package/dist/types/src/types/TriggerEvent.d.ts +43 -13
  32. package/dist/types/src/types/TriggerEvent.d.ts.map +1 -1
  33. package/dist/types/src/types/url.d.ts +4 -3
  34. package/dist/types/src/types/url.d.ts.map +1 -1
  35. package/dist/types/tsconfig.tsbuildinfo +1 -1
  36. package/package.json +23 -17
  37. package/src/errors.ts +4 -4
  38. package/src/operation-compatibility.test.ts +185 -0
  39. package/src/protocol/functions-ai-http-client.ts +67 -0
  40. package/src/protocol/protocol.ts +184 -67
  41. package/src/sdk.ts +68 -5
  42. package/src/services/credentials.ts +31 -15
  43. package/src/services/event-logger.ts +2 -2
  44. package/src/services/function-invocation-service.ts +14 -0
  45. package/src/services/index.ts +0 -2
  46. package/src/services/queues.ts +5 -7
  47. package/src/services/tracing.ts +63 -4
  48. package/src/types/Function.ts +28 -8
  49. package/src/types/Script.ts +8 -7
  50. package/src/types/Trigger.ts +18 -14
  51. package/src/types/TriggerEvent.ts +29 -29
  52. package/src/types/url.ts +4 -3
  53. package/dist/lib/browser/index.mjs.map +0 -7
  54. package/dist/lib/browser/meta.json +0 -1
  55. package/dist/lib/node-esm/index.mjs +0 -928
  56. package/dist/lib/node-esm/index.mjs.map +0 -7
  57. package/dist/lib/node-esm/meta.json +0 -1
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "@dxos/functions",
3
- "version": "0.8.4-main.e8ec1fe",
3
+ "version": "0.8.4-main.ef1bc66f44",
4
4
  "description": "Functions API.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/dxos/dxos"
10
+ },
7
11
  "license": "MIT",
8
12
  "author": "info@dxos.org",
9
13
  "sideEffects": false,
@@ -12,8 +16,7 @@
12
16
  ".": {
13
17
  "source": "./src/index.ts",
14
18
  "types": "./dist/types/src/index.d.ts",
15
- "browser": "./dist/lib/browser/index.mjs",
16
- "node": "./dist/lib/node-esm/index.mjs"
19
+ "default": "./dist/lib/neutral/index.mjs"
17
20
  }
18
21
  },
19
22
  "types": "dist/types/src/index.d.ts",
@@ -23,20 +26,23 @@
23
26
  "src"
24
27
  ],
25
28
  "dependencies": {
26
- "@effect/platform": "0.92.1",
27
- "effect": "3.18.3",
28
- "@dxos/ai": "0.8.4-main.e8ec1fe",
29
- "@dxos/echo-db": "0.8.4-main.e8ec1fe",
30
- "@dxos/effect": "0.8.4-main.e8ec1fe",
31
- "@dxos/errors": "0.8.4-main.e8ec1fe",
32
- "@dxos/invariant": "0.8.4-main.e8ec1fe",
33
- "@dxos/echo": "0.8.4-main.e8ec1fe",
34
- "@dxos/keys": "0.8.4-main.e8ec1fe",
35
- "@dxos/node-std": "0.8.4-main.e8ec1fe",
36
- "@dxos/log": "0.8.4-main.e8ec1fe",
37
- "@dxos/schema": "0.8.4-main.e8ec1fe",
38
- "@dxos/types": "0.8.4-main.e8ec1fe",
39
- "@dxos/protocols": "0.8.4-main.e8ec1fe"
29
+ "@effect/ai-anthropic": "0.23.0",
30
+ "@effect/platform": "0.94.4",
31
+ "effect": "3.19.16",
32
+ "@dxos/context": "0.8.4-main.ef1bc66f44",
33
+ "@dxos/ai": "0.8.4-main.ef1bc66f44",
34
+ "@dxos/echo-db": "0.8.4-main.ef1bc66f44",
35
+ "@dxos/effect": "0.8.4-main.ef1bc66f44",
36
+ "@dxos/echo": "0.8.4-main.ef1bc66f44",
37
+ "@dxos/invariant": "0.8.4-main.ef1bc66f44",
38
+ "@dxos/errors": "0.8.4-main.ef1bc66f44",
39
+ "@dxos/keys": "0.8.4-main.ef1bc66f44",
40
+ "@dxos/node-std": "0.8.4-main.ef1bc66f44",
41
+ "@dxos/operation": "0.8.4-main.ef1bc66f44",
42
+ "@dxos/log": "0.8.4-main.ef1bc66f44",
43
+ "@dxos/schema": "0.8.4-main.ef1bc66f44",
44
+ "@dxos/protocols": "0.8.4-main.ef1bc66f44",
45
+ "@dxos/types": "0.8.4-main.ef1bc66f44"
40
46
  },
41
47
  "publishConfig": {
42
48
  "access": "public"
package/src/errors.ts CHANGED
@@ -4,18 +4,18 @@
4
4
 
5
5
  import { BaseError, type BaseErrorOptions } from '@dxos/errors';
6
6
 
7
- export class ServiceNotAvailableError extends BaseError.extend('SERVICE_NOT_AVAILABLE', 'Service not available') {
7
+ export class ServiceNotAvailableError extends BaseError.extend('ServiceNotAvailable', 'Service not available') {
8
8
  constructor(service: string, options?: Omit<BaseErrorOptions, 'context'>) {
9
9
  super({ context: { service }, ...options });
10
10
  }
11
11
  }
12
12
 
13
- export class FunctionNotFoundError extends BaseError.extend('FUNCTION_NOT_FOUND', 'Function not found') {
13
+ export class FunctionNotFoundError extends BaseError.extend('FunctionNotFound', 'Function not found') {
14
14
  constructor(functionKey: string, options?: Omit<BaseErrorOptions, 'context'>) {
15
15
  super({ context: { function: functionKey }, ...options });
16
16
  }
17
17
  }
18
18
 
19
- export class FunctionError extends BaseError.extend('FUNCTION_ERROR', 'Function invocation error') {}
19
+ export class FunctionError extends BaseError.extend('FunctionError', 'Function invocation error') {}
20
20
 
21
- export class TriggerStateNotFoundError extends BaseError.extend('TRIGGER_STATE_NOT_FOUND', 'Trigger state not found') {}
21
+ export class TriggerStateNotFoundError extends BaseError.extend('TriggerStateNotFound', 'Trigger state not found') {}
@@ -0,0 +1,185 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Effect from 'effect/Effect';
6
+ import * as Schema from 'effect/Schema';
7
+ import { describe, expect, test } from 'vitest';
8
+
9
+ import { FunctionDefinition, defineFunction } from './sdk';
10
+
11
+ describe('Function/Operation Compatibility', () => {
12
+ test('can convert FunctionDefinition to OperationDefinition', () => {
13
+ const func = defineFunction({
14
+ key: 'test.function',
15
+ name: 'Test Function',
16
+ description: 'A test function',
17
+ inputSchema: Schema.Struct({ value: Schema.Number }),
18
+ outputSchema: Schema.Struct({ result: Schema.String }),
19
+ handler: async ({ data }) => {
20
+ return { result: data.value.toString() };
21
+ },
22
+ });
23
+
24
+ const op = FunctionDefinition.toOperation(func);
25
+
26
+ expect(op.meta.key).toBe('test.function');
27
+ expect(op.meta.name).toBe('Test Function');
28
+ expect(op.meta.description).toBe('A test function');
29
+ expect(Schema.isSchema(op.schema.input)).toBe(true);
30
+ expect(Schema.isSchema(op.schema.output)).toBe(true);
31
+ });
32
+
33
+ test('converted operation has matching schemas', () => {
34
+ const inputSchema = Schema.Struct({
35
+ name: Schema.String,
36
+ age: Schema.Number,
37
+ });
38
+ const outputSchema = Schema.Struct({
39
+ greeting: Schema.String,
40
+ });
41
+
42
+ const func = defineFunction({
43
+ key: 'test.greet',
44
+ name: 'Greet',
45
+ inputSchema,
46
+ outputSchema,
47
+ handler: async ({ data }) => {
48
+ return { greeting: `Hello, ${data.name}!` };
49
+ },
50
+ });
51
+
52
+ const op = FunctionDefinition.toOperation(func);
53
+
54
+ // Verify schemas match
55
+ const testInput = { name: 'Alice', age: 30 };
56
+ const validatedInput = Schema.decodeSync(op.schema.input)(testInput);
57
+ expect(validatedInput).toEqual(testInput);
58
+
59
+ const testOutput = { greeting: 'Hello, Alice!' };
60
+ const validatedOutput = Schema.decodeSync(op.schema.output)(testOutput);
61
+ expect(validatedOutput).toEqual(testOutput);
62
+ });
63
+
64
+ test('converted operation preserves metadata', () => {
65
+ const func = defineFunction({
66
+ key: 'test.meta',
67
+ name: 'Meta Test',
68
+ description: 'Tests metadata preservation',
69
+ inputSchema: Schema.Void,
70
+ outputSchema: Schema.Void,
71
+ handler: async () => {},
72
+ });
73
+
74
+ const op = FunctionDefinition.toOperation(func);
75
+
76
+ expect(op.meta.key).toBe('test.meta');
77
+ expect(op.meta.name).toBe('Meta Test');
78
+ expect(op.meta.description).toBe('Tests metadata preservation');
79
+ });
80
+
81
+ test('converted operation is pipeable', () => {
82
+ const func = defineFunction({
83
+ key: 'test.pipe',
84
+ name: 'Pipe Test',
85
+ inputSchema: Schema.Void,
86
+ outputSchema: Schema.Void,
87
+ handler: async () => {},
88
+ });
89
+
90
+ const op = FunctionDefinition.toOperation(func);
91
+
92
+ expect(typeof op.pipe).toBe('function');
93
+ });
94
+
95
+ test('handles functions without output schema', () => {
96
+ const func = defineFunction({
97
+ key: 'test.no-output',
98
+ name: 'No Output',
99
+ inputSchema: Schema.Struct({ value: Schema.Number }),
100
+ handler: async ({ data }) => {
101
+ return data.value * 2;
102
+ },
103
+ });
104
+
105
+ const op = FunctionDefinition.toOperation(func);
106
+
107
+ expect(op.meta.key).toBe('test.no-output');
108
+ expect(Schema.isSchema(op.schema.output)).toBe(true);
109
+ });
110
+
111
+ test('converted operation includes handler', () => {
112
+ const func = defineFunction({
113
+ key: 'test.handler',
114
+ name: 'Handler Test',
115
+ inputSchema: Schema.Struct({ value: Schema.Number }),
116
+ outputSchema: Schema.Struct({ doubled: Schema.Number }),
117
+ handler: async ({ data }) => {
118
+ return { doubled: data.value * 2 };
119
+ },
120
+ });
121
+
122
+ const op = FunctionDefinition.toOperation(func);
123
+
124
+ expect(op.handler).toBeDefined();
125
+ expect(typeof op.handler).toBe('function');
126
+ });
127
+
128
+ test('converted operation has handler function', () => {
129
+ const func = defineFunction({
130
+ key: 'test.execute',
131
+ name: 'Execute Test',
132
+ inputSchema: Schema.Struct({ value: Schema.Number }),
133
+ outputSchema: Schema.Struct({ result: Schema.Number }),
134
+ handler: async ({ data }) => {
135
+ return { result: data.value * 3 };
136
+ },
137
+ });
138
+
139
+ const op = FunctionDefinition.toOperation(func);
140
+
141
+ // Handler is present and is a function that returns an Effect
142
+ expect(op.handler).toBeDefined();
143
+ const effect = op.handler({ value: 5 });
144
+ // Verify it returns an Effect-like object
145
+ expect(effect).toHaveProperty('pipe');
146
+ });
147
+
148
+ test('converted operation works with Effect-based handlers', () => {
149
+ const func = defineFunction({
150
+ key: 'test.effect',
151
+ name: 'Effect Test',
152
+ inputSchema: Schema.Struct({ value: Schema.Number }),
153
+ outputSchema: Schema.Struct({ result: Schema.Number }),
154
+ handler: Effect.fn(function* ({ data }) {
155
+ return { result: data.value * 4 };
156
+ }),
157
+ });
158
+
159
+ const op = FunctionDefinition.toOperation(func);
160
+
161
+ // Handler is present and produces an Effect
162
+ expect(op.handler).toBeDefined();
163
+ const effect = op.handler({ value: 3 });
164
+ expect(effect).toHaveProperty('pipe');
165
+ });
166
+
167
+ test('converted operation works with synchronous handlers', () => {
168
+ const func = defineFunction({
169
+ key: 'test.sync',
170
+ name: 'Sync Test',
171
+ inputSchema: Schema.Struct({ value: Schema.Number }),
172
+ outputSchema: Schema.Struct({ result: Schema.Number }),
173
+ handler: ({ data }) => {
174
+ return { result: data.value * 5 };
175
+ },
176
+ });
177
+
178
+ const op = FunctionDefinition.toOperation(func);
179
+
180
+ // Handler is present and produces an Effect
181
+ expect(op.handler).toBeDefined();
182
+ const effect = op.handler({ value: 2 });
183
+ expect(effect).toHaveProperty('pipe');
184
+ });
185
+ });
@@ -0,0 +1,67 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Headers from '@effect/platform/Headers';
6
+ import * as HttpClient from '@effect/platform/HttpClient';
7
+ import * as HttpClientError from '@effect/platform/HttpClientError';
8
+ import * as HttpClientResponse from '@effect/platform/HttpClientResponse';
9
+ import * as Effect from 'effect/Effect';
10
+ import * as FiberRef from 'effect/FiberRef';
11
+ import * as Layer from 'effect/Layer';
12
+ import * as Stream from 'effect/Stream';
13
+
14
+ import { log } from '@dxos/log';
15
+ import { type EdgeFunctionEnv, ErrorCodec } from '@dxos/protocols';
16
+ /**
17
+ * Copy pasted from https://github.com/Effect-TS/effect/blob/main/packages/platform/src/internal/fetchHttpClient.ts
18
+ */
19
+ export const requestInitTagKey = '@effect/platform/FetchHttpClient/FetchOptions';
20
+
21
+ export class FunctionsAiHttpClient {
22
+ static make = (service: EdgeFunctionEnv.FunctionsAiService) =>
23
+ HttpClient.make((request, url, signal, fiber) => {
24
+ const context = fiber.getFiberRef(FiberRef.currentContext);
25
+ const options: RequestInit = context.unsafeMap.get(requestInitTagKey) ?? {};
26
+ const headers = options.headers
27
+ ? Headers.merge(Headers.fromInput(options.headers), request.headers)
28
+ : request.headers;
29
+
30
+ const send = (body: BodyInit | undefined) =>
31
+ Effect.tryPromise({
32
+ try: () =>
33
+ service.fetch(
34
+ new Request(url, {
35
+ ...options,
36
+ method: request.method,
37
+ headers,
38
+ body,
39
+ // Note: Don't pass signal - it can't be serialized through RPC
40
+ }),
41
+ ),
42
+ catch: (cause) => {
43
+ log.error('Failed to fetch', { errorSerialized: ErrorCodec.encode(cause as Error) });
44
+ return new HttpClientError.RequestError({
45
+ request,
46
+ reason: 'Transport',
47
+ cause,
48
+ });
49
+ },
50
+ }).pipe(Effect.map((response) => HttpClientResponse.fromWeb(request, response)));
51
+
52
+ switch (request.body._tag) {
53
+ case 'Raw':
54
+ case 'Uint8Array':
55
+ return send(request.body.body as any);
56
+ case 'FormData':
57
+ return send(request.body.formData);
58
+ case 'Stream':
59
+ return Stream.toReadableStreamEffect(request.body.stream).pipe(Effect.flatMap(send));
60
+ }
61
+
62
+ return send(undefined);
63
+ });
64
+
65
+ static layer = (service: EdgeFunctionEnv.FunctionsAiService) =>
66
+ Layer.succeed(HttpClient.HttpClient, FunctionsAiHttpClient.make(service));
67
+ }
@@ -2,23 +2,28 @@
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 } from '@dxos/ai';
12
+ import { AnthropicResolver } from '@dxos/ai/resolvers';
13
+ import { LifecycleState, Resource } from '@dxos/context';
14
+ import { Database, Ref, Type } from '@dxos/echo';
15
+ import { refFromEncodedReference } from '@dxos/echo/internal';
16
+ import { EchoClient, type EchoDatabaseImpl, type QueueFactory } from '@dxos/echo-db';
17
+ import { runAndForwardErrors } from '@dxos/effect';
18
+ import { assertState, failedInvariant, invariant } from '@dxos/invariant';
15
19
  import { PublicKey } from '@dxos/keys';
16
20
  import { type FunctionProtocol } from '@dxos/protocols';
17
21
 
18
22
  import { FunctionError } from '../errors';
19
23
  import { FunctionDefinition, type FunctionServices } from '../sdk';
20
- import { CredentialsService, DatabaseService, FunctionInvocationService, TracingService } from '../services';
21
- import { QueueService } from '../services';
24
+ import { CredentialsService, FunctionInvocationService, QueueService, TracingService } from '../services';
25
+
26
+ import { FunctionsAiHttpClient } from './functions-ai-http-client';
22
27
 
23
28
  /**
24
29
  * Wraps a function handler made with `defineFunction` to a protocol that the functions-runtime expects.
@@ -39,7 +44,7 @@ export const wrapFunctionHandler = (func: FunctionDefinition): FunctionProtocol.
39
44
  },
40
45
  handler: async ({ data, context }) => {
41
46
  if (
42
- (func.services.includes(DatabaseService.key) || func.services.includes(QueueService.key)) &&
47
+ (func.services.includes(Database.Service.key) || func.services.includes(QueueService.key)) &&
43
48
  (!context.services.dataService || !context.services.queryService)
44
49
  ) {
45
50
  throw new FunctionError({
@@ -47,22 +52,39 @@ export const wrapFunctionHandler = (func: FunctionDefinition): FunctionProtocol.
47
52
  });
48
53
  }
49
54
 
55
+ // eslint-disable-next-line no-useless-catch
50
56
  try {
51
57
  if (!SchemaAST.isAnyKeyword(func.inputSchema.ast)) {
52
- Schema.validateSync(func.inputSchema)(data);
58
+ try {
59
+ Schema.validateSync(func.inputSchema)(data);
60
+ } catch (error) {
61
+ throw new FunctionError({ message: 'Invalid input schema', cause: error });
62
+ }
63
+ }
64
+
65
+ await using funcContext = await new FunctionContext(context).open();
66
+
67
+ if (func.types.length > 0) {
68
+ invariant(funcContext.db, 'Database is required for functions with types');
69
+ await funcContext.db.graph.schemaRegistry.register(func.types as Type.Entity.Any[]);
53
70
  }
54
71
 
72
+ const dataWithDecodedRefs =
73
+ funcContext.db && !SchemaAST.isAnyKeyword(func.inputSchema.ast)
74
+ ? decodeRefsFromSchema(func.inputSchema.ast, data, funcContext.db)
75
+ : data;
76
+
55
77
  let result = await func.handler({
56
78
  // TODO(dmaretskyi): Fix the types.
57
79
  context: context as any,
58
- data,
80
+ data: dataWithDecodedRefs,
59
81
  });
60
82
 
61
83
  if (Effect.isEffect(result)) {
62
- result = await Effect.runPromise(
84
+ result = await runAndForwardErrors(
63
85
  (result as Effect.Effect<unknown, unknown, FunctionServices>).pipe(
64
86
  Effect.orDie,
65
- Effect.provide(createServiceLayer(context)),
87
+ Effect.provide(funcContext.createLayer()),
66
88
  ),
67
89
  );
68
90
  }
@@ -73,73 +95,168 @@ export const wrapFunctionHandler = (func: FunctionDefinition): FunctionProtocol.
73
95
 
74
96
  return result;
75
97
  } 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
- }
98
+ // TODO(dmaretskyi): We might do error wrapping here and add extra context.
99
+ throw error;
84
100
  }
85
101
  },
86
102
  };
87
103
  };
88
104
 
89
105
  /**
90
- * Creates a layer of services for the function.
106
+ * Container for services and context for a function.
91
107
  */
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
- });
106
- });
107
- }
108
+ class FunctionContext extends Resource {
109
+ readonly context: FunctionProtocol.Context;
110
+ readonly client: EchoClient | undefined;
111
+ db: EchoDatabaseImpl | undefined;
112
+ queues: QueueFactory | undefined;
108
113
 
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
- );
125
- }
114
+ constructor(context: FunctionProtocol.Context) {
115
+ super();
116
+ this.context = context;
117
+ if (context.services.dataService && context.services.queryService) {
118
+ this.client = new EchoClient().connectToService({
119
+ dataService: context.services.dataService,
120
+ queryService: context.services.queryService,
121
+ queueService: context.services.queueService,
122
+ });
123
+ }
124
+ }
126
125
 
127
- const queues = client && context.spaceId ? client.constructQueueFactory(context.spaceId) : undefined;
126
+ override async _open() {
127
+ await this.client?.open();
128
+ this.db =
129
+ this.client && this.context.spaceId
130
+ ? this.client.constructDatabase({
131
+ spaceId: this.context.spaceId ?? failedInvariant(),
132
+ spaceKey: PublicKey.fromHex(this.context.spaceKey ?? failedInvariant('spaceKey missing in context')),
133
+ reactiveSchemaQuery: false,
134
+ preloadSchemaOnOpen: false,
135
+ })
136
+ : undefined;
128
137
 
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;
138
+ await this.db?.setSpaceRoot(this.context.spaceRootUrl ?? failedInvariant('spaceRootUrl missing in context'));
139
+ await this.db?.open();
140
+ this.queues =
141
+ this.client && this.context.spaceId ? this.client.constructQueueFactory(this.context.spaceId) : undefined;
142
+ }
137
143
 
138
- return Layer.mergeAll(dbLayer, queuesLayer, credentials, functionInvocationService, aiService, tracing);
139
- }),
140
- );
141
- };
144
+ override async _close() {
145
+ await this.db?.close();
146
+ await this.client?.close();
147
+ }
148
+
149
+ createLayer(): Layer.Layer<FunctionServices> {
150
+ assertState(this._lifecycleState === LifecycleState.OPEN, 'FunctionContext is not open');
151
+
152
+ const dbLayer = this.db ? Database.layer(this.db) : Database.notAvailable;
153
+ const queuesLayer = this.queues ? QueueService.layer(this.queues) : QueueService.notAvailable;
154
+ const credentials = dbLayer
155
+ ? CredentialsService.layerFromDatabase({ caching: true }).pipe(Layer.provide(dbLayer))
156
+ : CredentialsService.configuredLayer([]);
157
+ const functionInvocationService = MockedFunctionInvocationService;
158
+ const tracing = TracingService.layerNoop;
159
+
160
+ const aiLayer = this.context.services.functionsAiService
161
+ ? AiModelResolver.AiModelResolver.buildAiService.pipe(
162
+ Layer.provide(
163
+ AnthropicResolver.make().pipe(
164
+ Layer.provide(
165
+ AnthropicClient.layer({
166
+ // Note: It doesn't matter what is base url here, it will be proxied to ai gateway in edge.
167
+ apiUrl: 'http://internal/provider/anthropic',
168
+ }).pipe(Layer.provide(FunctionsAiHttpClient.layer(this.context.services.functionsAiService))),
169
+ ),
170
+ ),
171
+ ),
172
+ )
173
+ : AiService.notAvailable;
174
+
175
+ return Layer.mergeAll(
176
+ dbLayer, //
177
+ queuesLayer,
178
+ credentials,
179
+ functionInvocationService,
180
+ aiLayer,
181
+ tracing,
182
+ );
183
+ }
184
+ }
142
185
 
143
186
  const MockedFunctionInvocationService = Layer.succeed(FunctionInvocationService, {
144
187
  invokeFunction: () => Effect.die('Calling functions from functions is not implemented yet.'),
188
+ resolveFunction: () => Effect.die('Not implemented.'),
145
189
  });
190
+
191
+ const decodeRefsFromSchema = (ast: SchemaAST.AST, value: unknown, db: EchoDatabaseImpl): unknown => {
192
+ if (value == null) {
193
+ return value;
194
+ }
195
+
196
+ const encoded = SchemaAST.encodedBoundAST(ast);
197
+ if (Ref.isRefType(encoded)) {
198
+ if (Ref.isRef(value)) {
199
+ return value;
200
+ }
201
+
202
+ if (typeof value === 'object' && value !== null && typeof (value as any)['/'] === 'string') {
203
+ const resolver = db.graph.createRefResolver({ context: { space: db.spaceId } });
204
+ return refFromEncodedReference(value as any, resolver);
205
+ }
206
+
207
+ return value;
208
+ }
209
+
210
+ switch (encoded._tag) {
211
+ case 'TypeLiteral': {
212
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
213
+ return value;
214
+ }
215
+ const result: Record<string, unknown> = { ...(value as any) };
216
+ for (const prop of SchemaAST.getPropertySignatures(encoded)) {
217
+ const key = prop.name.toString();
218
+ if (key in result) {
219
+ result[key] = decodeRefsFromSchema(prop.type, (result as any)[key], db);
220
+ }
221
+ }
222
+ return result;
223
+ }
224
+
225
+ case 'TupleType': {
226
+ if (!Array.isArray(value)) {
227
+ return value;
228
+ }
229
+
230
+ // For arrays, effect uses TupleType with empty elements and a single rest element.
231
+ if (encoded.elements.length === 0 && encoded.rest.length === 1) {
232
+ const elementType = encoded.rest[0].type;
233
+ return (value as unknown[]).map((item) => decodeRefsFromSchema(elementType, item, db));
234
+ }
235
+
236
+ return value;
237
+ }
238
+
239
+ case 'Union': {
240
+ // Optional values are represented as union with undefined.
241
+ const nonUndefined = encoded.types.filter((t) => !SchemaAST.isUndefinedKeyword(t));
242
+ if (nonUndefined.length === 1) {
243
+ return decodeRefsFromSchema(nonUndefined[0], value, db);
244
+ }
245
+
246
+ // For other unions we can't safely pick a branch without validating.
247
+ return value;
248
+ }
249
+
250
+ case 'Suspend': {
251
+ return decodeRefsFromSchema(encoded.f(), value, db);
252
+ }
253
+
254
+ case 'Refinement': {
255
+ return decodeRefsFromSchema(encoded.from, value, db);
256
+ }
257
+
258
+ default: {
259
+ return value;
260
+ }
261
+ }
262
+ };