@dxos/functions 0.8.4-main.dedc0f3 → 0.8.4-main.dfabb4ec29

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 (181) hide show
  1. package/README.md +4 -6
  2. package/dist/lib/neutral/index.mjs +593 -0
  3. package/dist/lib/neutral/index.mjs.map +7 -0
  4. package/dist/lib/neutral/meta.json +1 -0
  5. package/dist/types/src/index.d.ts +3 -9
  6. package/dist/types/src/index.d.ts.map +1 -1
  7. package/dist/types/src/protocol/functions-ai-http-client.d.ts +12 -0
  8. package/dist/types/src/protocol/functions-ai-http-client.d.ts.map +1 -0
  9. package/dist/types/src/protocol/functions-ai-http-client.test.d.ts +2 -0
  10. package/dist/types/src/protocol/functions-ai-http-client.test.d.ts.map +1 -0
  11. package/dist/types/src/protocol/index.d.ts +2 -0
  12. package/dist/types/src/protocol/index.d.ts.map +1 -0
  13. package/dist/types/src/protocol/protocol.d.ts +19 -0
  14. package/dist/types/src/protocol/protocol.d.ts.map +1 -0
  15. package/dist/types/src/protocol/protocol.test.d.ts +2 -0
  16. package/dist/types/src/protocol/protocol.test.d.ts.map +1 -0
  17. package/dist/types/src/sdk.d.ts +10 -0
  18. package/dist/types/src/sdk.d.ts.map +1 -0
  19. package/dist/types/src/services/credentials.d.ts +22 -39
  20. package/dist/types/src/services/credentials.d.ts.map +1 -1
  21. package/dist/types/src/services/function-invocation-service.d.ts +15 -0
  22. package/dist/types/src/services/function-invocation-service.d.ts.map +1 -0
  23. package/dist/types/src/services/index.d.ts +3 -7
  24. package/dist/types/src/services/index.d.ts.map +1 -1
  25. package/dist/types/src/services/queues.d.ts +1 -44
  26. package/dist/types/src/services/queues.d.ts.map +1 -1
  27. package/dist/types/src/services/tracing.d.ts +2 -54
  28. package/dist/types/src/services/tracing.d.ts.map +1 -1
  29. package/dist/types/src/types/index.d.ts +2 -0
  30. package/dist/types/src/types/index.d.ts.map +1 -0
  31. package/dist/types/src/types/url.d.ts +13 -0
  32. package/dist/types/src/types/url.d.ts.map +1 -0
  33. package/dist/types/tsconfig.tsbuildinfo +1 -1
  34. package/package.json +25 -72
  35. package/src/index.ts +3 -9
  36. package/src/protocol/functions-ai-http-client.test.ts +105 -0
  37. package/src/protocol/functions-ai-http-client.ts +141 -0
  38. package/src/{executor → protocol}/index.ts +1 -1
  39. package/src/protocol/protocol.test.ts +58 -0
  40. package/src/protocol/protocol.ts +428 -0
  41. package/src/sdk.ts +31 -0
  42. package/src/services/credentials.ts +90 -111
  43. package/src/services/function-invocation-service.ts +33 -0
  44. package/src/services/index.ts +3 -7
  45. package/src/services/queues.ts +1 -78
  46. package/src/services/tracing.ts +1 -133
  47. package/src/types/index.ts +5 -0
  48. package/src/types/url.ts +32 -0
  49. package/dist/lib/browser/bundler/index.mjs +0 -265
  50. package/dist/lib/browser/bundler/index.mjs.map +0 -7
  51. package/dist/lib/browser/chunk-ANP3DFCO.mjs +0 -623
  52. package/dist/lib/browser/chunk-ANP3DFCO.mjs.map +0 -7
  53. package/dist/lib/browser/chunk-J5LGTIGS.mjs +0 -10
  54. package/dist/lib/browser/chunk-J5LGTIGS.mjs.map +0 -7
  55. package/dist/lib/browser/edge/index.mjs +0 -83
  56. package/dist/lib/browser/edge/index.mjs.map +0 -7
  57. package/dist/lib/browser/index.mjs +0 -1265
  58. package/dist/lib/browser/index.mjs.map +0 -7
  59. package/dist/lib/browser/meta.json +0 -1
  60. package/dist/lib/browser/testing/index.mjs +0 -129
  61. package/dist/lib/browser/testing/index.mjs.map +0 -7
  62. package/dist/lib/node-esm/bundler/index.mjs +0 -266
  63. package/dist/lib/node-esm/bundler/index.mjs.map +0 -7
  64. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
  65. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
  66. package/dist/lib/node-esm/chunk-MPKVY7ZR.mjs +0 -625
  67. package/dist/lib/node-esm/chunk-MPKVY7ZR.mjs.map +0 -7
  68. package/dist/lib/node-esm/edge/index.mjs +0 -84
  69. package/dist/lib/node-esm/edge/index.mjs.map +0 -7
  70. package/dist/lib/node-esm/index.mjs +0 -1266
  71. package/dist/lib/node-esm/index.mjs.map +0 -7
  72. package/dist/lib/node-esm/meta.json +0 -1
  73. package/dist/lib/node-esm/testing/index.mjs +0 -130
  74. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  75. package/dist/types/src/bundler/bundler.d.ts +0 -49
  76. package/dist/types/src/bundler/bundler.d.ts.map +0 -1
  77. package/dist/types/src/bundler/bundler.test.d.ts +0 -2
  78. package/dist/types/src/bundler/bundler.test.d.ts.map +0 -1
  79. package/dist/types/src/bundler/index.d.ts +0 -2
  80. package/dist/types/src/bundler/index.d.ts.map +0 -1
  81. package/dist/types/src/edge/functions.d.ts +0 -17
  82. package/dist/types/src/edge/functions.d.ts.map +0 -1
  83. package/dist/types/src/edge/index.d.ts +0 -2
  84. package/dist/types/src/edge/index.d.ts.map +0 -1
  85. package/dist/types/src/errors.d.ts +0 -137
  86. package/dist/types/src/errors.d.ts.map +0 -1
  87. package/dist/types/src/examples/fib.d.ts +0 -7
  88. package/dist/types/src/examples/fib.d.ts.map +0 -1
  89. package/dist/types/src/examples/index.d.ts +0 -4
  90. package/dist/types/src/examples/index.d.ts.map +0 -1
  91. package/dist/types/src/examples/reply.d.ts +0 -3
  92. package/dist/types/src/examples/reply.d.ts.map +0 -1
  93. package/dist/types/src/examples/sleep.d.ts +0 -5
  94. package/dist/types/src/examples/sleep.d.ts.map +0 -1
  95. package/dist/types/src/executor/executor.d.ts +0 -11
  96. package/dist/types/src/executor/executor.d.ts.map +0 -1
  97. package/dist/types/src/executor/index.d.ts +0 -2
  98. package/dist/types/src/executor/index.d.ts.map +0 -1
  99. package/dist/types/src/handler.d.ts +0 -94
  100. package/dist/types/src/handler.d.ts.map +0 -1
  101. package/dist/types/src/schema.d.ts +0 -43
  102. package/dist/types/src/schema.d.ts.map +0 -1
  103. package/dist/types/src/services/database.d.ts +0 -98
  104. package/dist/types/src/services/database.d.ts.map +0 -1
  105. package/dist/types/src/services/event-logger.d.ts +0 -72
  106. package/dist/types/src/services/event-logger.d.ts.map +0 -1
  107. package/dist/types/src/services/local-function-execution.d.ts +0 -25
  108. package/dist/types/src/services/local-function-execution.d.ts.map +0 -1
  109. package/dist/types/src/services/remote-function-execution-service.d.ts +0 -15
  110. package/dist/types/src/services/remote-function-execution-service.d.ts.map +0 -1
  111. package/dist/types/src/services/service-container.d.ts +0 -56
  112. package/dist/types/src/services/service-container.d.ts.map +0 -1
  113. package/dist/types/src/services/service-registry.d.ts +0 -29
  114. package/dist/types/src/services/service-registry.d.ts.map +0 -1
  115. package/dist/types/src/services/service-registry.test.d.ts +0 -2
  116. package/dist/types/src/services/service-registry.test.d.ts.map +0 -1
  117. package/dist/types/src/testing/index.d.ts +0 -3
  118. package/dist/types/src/testing/index.d.ts.map +0 -1
  119. package/dist/types/src/testing/layer.d.ts +0 -15
  120. package/dist/types/src/testing/layer.d.ts.map +0 -1
  121. package/dist/types/src/testing/logger.d.ts +0 -5
  122. package/dist/types/src/testing/logger.d.ts.map +0 -1
  123. package/dist/types/src/testing/persist-database.test.d.ts +0 -2
  124. package/dist/types/src/testing/persist-database.test.d.ts.map +0 -1
  125. package/dist/types/src/testing/services.d.ts +0 -59
  126. package/dist/types/src/testing/services.d.ts.map +0 -1
  127. package/dist/types/src/trace.d.ts +0 -122
  128. package/dist/types/src/trace.d.ts.map +0 -1
  129. package/dist/types/src/translations.d.ts +0 -12
  130. package/dist/types/src/translations.d.ts.map +0 -1
  131. package/dist/types/src/triggers/index.d.ts +0 -4
  132. package/dist/types/src/triggers/index.d.ts.map +0 -1
  133. package/dist/types/src/triggers/input-builder.d.ts +0 -3
  134. package/dist/types/src/triggers/input-builder.d.ts.map +0 -1
  135. package/dist/types/src/triggers/invocation-tracer.d.ts +0 -35
  136. package/dist/types/src/triggers/invocation-tracer.d.ts.map +0 -1
  137. package/dist/types/src/triggers/trigger-dispatcher.d.ts +0 -75
  138. package/dist/types/src/triggers/trigger-dispatcher.d.ts.map +0 -1
  139. package/dist/types/src/triggers/trigger-dispatcher.test.d.ts +0 -2
  140. package/dist/types/src/triggers/trigger-dispatcher.test.d.ts.map +0 -1
  141. package/dist/types/src/triggers/trigger-state-store.d.ts +0 -27
  142. package/dist/types/src/triggers/trigger-state-store.d.ts.map +0 -1
  143. package/dist/types/src/types.d.ts +0 -211
  144. package/dist/types/src/types.d.ts.map +0 -1
  145. package/dist/types/src/url.d.ts +0 -21
  146. package/dist/types/src/url.d.ts.map +0 -1
  147. package/src/bundler/bundler.test.ts +0 -58
  148. package/src/bundler/bundler.ts +0 -291
  149. package/src/bundler/index.ts +0 -5
  150. package/src/edge/functions.ts +0 -67
  151. package/src/edge/index.ts +0 -9
  152. package/src/errors.ts +0 -21
  153. package/src/examples/fib.ts +0 -30
  154. package/src/examples/index.ts +0 -7
  155. package/src/examples/reply.ts +0 -18
  156. package/src/examples/sleep.ts +0 -22
  157. package/src/executor/executor.ts +0 -54
  158. package/src/handler.ts +0 -201
  159. package/src/schema.ts +0 -68
  160. package/src/services/database.ts +0 -170
  161. package/src/services/event-logger.ts +0 -118
  162. package/src/services/local-function-execution.ts +0 -114
  163. package/src/services/remote-function-execution-service.ts +0 -46
  164. package/src/services/service-container.ts +0 -114
  165. package/src/services/service-registry.test.ts +0 -42
  166. package/src/services/service-registry.ts +0 -59
  167. package/src/testing/index.ts +0 -6
  168. package/src/testing/layer.ts +0 -111
  169. package/src/testing/logger.ts +0 -16
  170. package/src/testing/persist-database.test.ts +0 -87
  171. package/src/testing/services.ts +0 -115
  172. package/src/trace.ts +0 -178
  173. package/src/translations.ts +0 -20
  174. package/src/triggers/index.ts +0 -7
  175. package/src/triggers/input-builder.ts +0 -35
  176. package/src/triggers/invocation-tracer.ts +0 -99
  177. package/src/triggers/trigger-dispatcher.test.ts +0 -652
  178. package/src/triggers/trigger-dispatcher.ts +0 -512
  179. package/src/triggers/trigger-state-store.ts +0 -60
  180. package/src/types.ts +0 -200
  181. package/src/url.ts +0 -55
@@ -0,0 +1,428 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as AnthropicClient from '@effect/ai-anthropic/AnthropicClient';
6
+ import * as Effect from 'effect/Effect';
7
+ import * as Layer from 'effect/Layer';
8
+ import * as Option from 'effect/Option';
9
+ import * as Schema from 'effect/Schema';
10
+ import * as SchemaAST from 'effect/SchemaAST';
11
+
12
+ import { AiModelResolver, AiService, OpaqueToolkit } from '@dxos/ai';
13
+ import { AnthropicResolver } from '@dxos/ai/resolvers';
14
+ import {
15
+ FunctionError,
16
+ InvalidOperationInputError,
17
+ InvalidOperationOutputError,
18
+ Operation,
19
+ OperationRegistry,
20
+ Trace,
21
+ } from '@dxos/compute';
22
+ import { LifecycleState, Resource } from '@dxos/context';
23
+ import { Database, Feed, JsonSchema, Ref, type Type } from '@dxos/echo';
24
+ import { createFeedServiceLayer, EchoClient, type EchoDatabaseImpl, type QueueFactory } from '@dxos/echo-db';
25
+ import { refFromEncodedReference } from '@dxos/echo/internal';
26
+ import { runAndForwardErrors } from '@dxos/effect';
27
+ import { assertState, failedInvariant, invariant } from '@dxos/invariant';
28
+ import { PublicKey, type SpaceId } from '@dxos/keys';
29
+ import { log } from '@dxos/log';
30
+ import { EdgeFunctionEnv, ErrorCodec, type FunctionProtocol, type TraceProtocol } from '@dxos/protocols';
31
+
32
+ import { type FunctionServices } from '../sdk';
33
+ import {
34
+ configuredCredentialsLayer,
35
+ credentialsLayerFromDatabase,
36
+ FunctionInvocationService,
37
+ QueueService,
38
+ } 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
+ }
52
+
53
+ /**
54
+ * Wraps a function handler made with `defineFunction` to a protocol that the functions-runtime expects.
55
+ */
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');
62
+ }
63
+
64
+ const serviceTags = func.services.map((service) => service.key);
65
+
66
+ return {
67
+ meta: {
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),
74
+ },
75
+ handler: async ({ data, context }) => {
76
+ if (
77
+ (serviceTags.includes(Database.Service.key) ||
78
+ serviceTags.includes(QueueService.key) ||
79
+ serviceTags.includes(Feed.FeedService.key)) &&
80
+ (!context.services.dataService || !context.services.queryService)
81
+ ) {
82
+ throw new FunctionError({
83
+ message: 'Services not provided: dataService, queryService',
84
+ });
85
+ }
86
+
87
+ // eslint-disable-next-line no-useless-catch
88
+ try {
89
+ if (!SchemaAST.isAnyKeyword(func.input.ast)) {
90
+ try {
91
+ Schema.validateSync(func.input, { onExcessProperty: 'error' })(data);
92
+ } catch (error: any) {
93
+ throw new InvalidOperationInputError({
94
+ message: `Operation input did not match schema (${func.meta.key}): ${error.message}`,
95
+ cause: error,
96
+ });
97
+ }
98
+ }
99
+
100
+ await using funcContext = await new FunctionContext(context, opts).open();
101
+
102
+ const types = [...(opts.types ?? []), ...(func.types ?? [])];
103
+ if (types.length > 0) {
104
+ invariant(funcContext.db, 'Database is required for functions with types');
105
+ await funcContext.db.graph.schemaRegistry.register(types as Type.AnyEntity[]);
106
+ }
107
+
108
+ const dataWithDecodedRefs =
109
+ funcContext.db && !SchemaAST.isAnyKeyword(func.input.ast)
110
+ ? decodeRefsFromSchema(func.input.ast, data, funcContext.db)
111
+ : data;
112
+
113
+ let result: any = await func.handler(dataWithDecodedRefs);
114
+
115
+ if (Effect.isEffect(result)) {
116
+ result = await runAndForwardErrors(
117
+ (result as Effect.Effect<unknown, unknown, FunctionServices>).pipe(
118
+ Effect.orDie,
119
+ Effect.provide(funcContext.createLayer()),
120
+ ),
121
+ );
122
+ }
123
+
124
+ if (func.output && !SchemaAST.isAnyKeyword(func.output.ast)) {
125
+ try {
126
+ Schema.validateSync(func.output, { onExcessProperty: 'error' })(result);
127
+ } catch (error: any) {
128
+ throw new InvalidOperationOutputError({
129
+ message: `Operation output did not match schema (${func.meta.key}): ${error.message}`,
130
+ cause: error,
131
+ });
132
+ }
133
+ }
134
+
135
+ return result;
136
+ } catch (error) {
137
+ // TODO(dmaretskyi): We might do error wrapping here and add extra context.
138
+ throw error;
139
+ }
140
+ },
141
+ };
142
+ };
143
+
144
+ /**
145
+ * Container for services and context for a function.
146
+ */
147
+ class FunctionContext extends Resource {
148
+ readonly context: FunctionProtocol.Context;
149
+ readonly client: EchoClient | undefined;
150
+ db: EchoDatabaseImpl | undefined;
151
+ queues: QueueFactory | undefined;
152
+ readonly opts: FunctionWrappingOptions;
153
+
154
+ constructor(context: FunctionProtocol.Context, opts: FunctionWrappingOptions) {
155
+ super();
156
+ this.context = context;
157
+ this.opts = opts;
158
+ if (context.services.dataService && context.services.queryService) {
159
+ this.client = new EchoClient().connectToService({
160
+ dataService: context.services.dataService,
161
+ queryService: context.services.queryService,
162
+ queueService: context.services.queueService,
163
+ });
164
+ }
165
+ }
166
+
167
+ override async _open() {
168
+ await this.client?.open();
169
+ this.db =
170
+ this.client && this.context.spaceId
171
+ ? this.client.constructDatabase({
172
+ spaceId: this.context.spaceId ?? failedInvariant(),
173
+ spaceKey: PublicKey.fromHex(this.context.spaceKey ?? failedInvariant('spaceKey missing in context')),
174
+ reactiveSchemaQuery: false,
175
+ preloadSchemaOnOpen: false,
176
+ })
177
+ : undefined;
178
+
179
+ await this.db?.setSpaceRoot(this.context.spaceRootUrl ?? failedInvariant('spaceRootUrl missing in context'));
180
+ await this.db?.open();
181
+ this.queues =
182
+ this.client && this.context.spaceId ? this.client.constructQueueFactory(this.context.spaceId) : undefined;
183
+ }
184
+
185
+ override async _close() {
186
+ await this.db?.close();
187
+ await this.client?.close();
188
+ }
189
+
190
+ createLayer(): Layer.Layer<FunctionServices> {
191
+ assertState(this._lifecycleState === LifecycleState.OPEN, 'FunctionContext is not open');
192
+
193
+ const dbLayer = this.db ? Database.layer(this.db) : Database.notAvailable;
194
+ const queuesLayer = this.queues ? QueueService.layer(this.queues) : QueueService.notAvailable;
195
+ const feedLayer = this.queues ? createFeedServiceLayer(this.queues) : Feed.notAvailable;
196
+ const credentials = dbLayer
197
+ ? credentialsLayerFromDatabase({ caching: true }).pipe(Layer.provide(dbLayer))
198
+ : configuredCredentialsLayer([]);
199
+
200
+ const aiLayer = this.context.services.functionsAiService
201
+ ? InternalAiServiceLayer(this.context.services.functionsAiService)
202
+ : AiService.notAvailable;
203
+
204
+ const operationServiceLayer = this.context.services.functionsService
205
+ ? makeOperationServiceLayer(this.context.services.functionsService)
206
+ : unavailableOperationServiceLayer;
207
+
208
+ const operationRegistryLayer = this.context.services.functionsService
209
+ ? makeOperationRegistryLayer(this.context.services.functionsService, this.context.spaceId as SpaceId | undefined)
210
+ : emptyOperationRegistryLayer;
211
+
212
+ const traceWriterLayer = this.context.services.traceService
213
+ ? makeTraceWriterLayer(this.context.services.traceService)
214
+ : Trace.writerLayerNoop;
215
+
216
+ log('Creating function context layer', {
217
+ traceService: !!this.context.services.traceService,
218
+ functionsService: !!this.context.services.functionsService,
219
+ functionsAiService: !!this.context.services.functionsAiService,
220
+ spaceId: this.context.spaceId,
221
+ spaceRootUrl: this.context.spaceRootUrl,
222
+ toolkits: this.opts.toolkits?.length ?? 0,
223
+ types: this.opts.types?.length ?? 0,
224
+ });
225
+
226
+ return Layer.mergeAll(
227
+ dbLayer,
228
+ queuesLayer,
229
+ feedLayer,
230
+ credentials,
231
+ operationServiceLayer,
232
+ operationRegistryLayer,
233
+ aiLayer,
234
+ OpaqueToolkit.providerLayer(OpaqueToolkit.merge(...(this.opts.toolkits ?? []))),
235
+ traceWriterLayer,
236
+
237
+ // `FunctionInvocationService` is deprecated; new code should yield `Operation.Service`.
238
+ // The cloudflare wrapper provides only the unavailable layer to satisfy the (still-present)
239
+ // type union — handlers that yield it will die at invocation time.
240
+ FunctionInvocationService.layerNotAvailable,
241
+ );
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Backs `Trace.TraceService` with the EDGE-provided `TraceService` so that operation
247
+ * handlers can write trace events that are forwarded to the runtime's trace sink.
248
+ */
249
+ const makeTraceWriterLayer = (traceService: TraceProtocol.TraceService): Layer.Layer<Trace.TraceService> =>
250
+ Layer.succeed(Trace.TraceService, {
251
+ write: (eventType, payload) => {
252
+ log('Writing trace event', {
253
+ eventType: eventType.key,
254
+ });
255
+ traceService.write([
256
+ {
257
+ key: eventType.key,
258
+ isEphemeral: eventType.isEphemeral,
259
+ data: payload,
260
+ },
261
+ ]);
262
+ },
263
+ });
264
+
265
+ /**
266
+ * AI service layer that proxies HTTP requests through the EDGE-provided `FunctionsAiService`.
267
+ */
268
+ const InternalAiServiceLayer = (functionsAiService: EdgeFunctionEnv.FunctionsAiService) =>
269
+ AiModelResolver.AiModelResolver.buildAiService.pipe(
270
+ Layer.provide(
271
+ AnthropicResolver.make().pipe(
272
+ Layer.provide(
273
+ AnthropicClient.layer({
274
+ // Note: It doesn't matter what is base url here, it will be proxied to ai gateway in edge.
275
+ apiUrl: 'http://internal/provider/anthropic',
276
+ }).pipe(Layer.provide(FunctionsAiHttpClient.layer(functionsAiService))),
277
+ ),
278
+ ),
279
+ ),
280
+ );
281
+
282
+ /**
283
+ * Backs `Operation.Service` with the EDGE-provided `FunctionsService` so that operation
284
+ * handlers can invoke other deployed operations remotely. The `deployedId` on the operation
285
+ * definition is used as the routing key.
286
+ */
287
+ const makeOperationServiceLayer = (
288
+ functionsService: EdgeFunctionEnv.FunctionsService,
289
+ ): Layer.Layer<Operation.Service> => {
290
+ const invokeRemote = async (
291
+ op: Operation.Definition.Any,
292
+ input: unknown,
293
+ options?: Operation.InvokeOptions,
294
+ ): Promise<{ data?: unknown; error?: Error }> => {
295
+ invariant(op.meta.deployedId, `Operation '${op.meta.key}' has no deployedId; cannot invoke remotely.`);
296
+ const result = await functionsService.invoke(op.meta.deployedId, input, {
297
+ spaceId: options?.spaceId,
298
+ });
299
+ if (result._kind === 'success') {
300
+ return { data: result.data };
301
+ }
302
+ return { error: ErrorCodec.decode(result.error) };
303
+ };
304
+
305
+ return Layer.succeed(Operation.Service, {
306
+ invoke: ((op: Operation.Definition.Any, input: unknown, options?: Operation.InvokeOptions) =>
307
+ Effect.tryPromise(() => invokeRemote(op, input, options)).pipe(
308
+ Effect.orDie,
309
+ Effect.flatMap((outcome) =>
310
+ outcome.error ? Effect.die(outcome.error) : Effect.succeed(outcome.data as never),
311
+ ),
312
+ )) as Operation.OperationService['invoke'],
313
+ schedule: ((op: Operation.Definition.Any, input: unknown) =>
314
+ Effect.sync(() => {
315
+ invariant(op.meta.deployedId, `Operation '${op.meta.key}' has no deployedId; cannot schedule remotely.`);
316
+ // Fire and forget — schedule is intentionally non-awaiting.
317
+ void functionsService.invoke(op.meta.deployedId, input).catch(() => {
318
+ // Swallow errors — schedule is observability-only.
319
+ });
320
+ })) as Operation.OperationService['schedule'],
321
+ invokePromise: ((op: Operation.Definition.Any, input: unknown, options?: Operation.InvokeOptions) =>
322
+ invokeRemote(op, input, options).catch((error: unknown) => ({
323
+ error: error instanceof Error ? error : new Error(String(error)),
324
+ }))) as Operation.OperationService['invokePromise'],
325
+ } satisfies Operation.OperationService);
326
+ };
327
+
328
+ const unavailableOperationServiceLayer = Layer.succeed(Operation.Service, {
329
+ invoke: () => Effect.die('Operation.Service is not available: missing functionsService in EDGE context.'),
330
+ schedule: () => Effect.die('Operation.Service is not available: missing functionsService in EDGE context.'),
331
+ invokePromise: async () => ({
332
+ error: new Error('Operation.Service is not available: missing functionsService in EDGE context.'),
333
+ }),
334
+ } as Operation.OperationService);
335
+
336
+ /**
337
+ * Backs `OperationRegistry.Service` with the EDGE-provided `FunctionsService.query`. Returns
338
+ * the first persistent operation matching the requested key, or `Option.none()` when not found.
339
+ */
340
+ const makeOperationRegistryLayer = (
341
+ functionsService: EdgeFunctionEnv.FunctionsService,
342
+ spaceId: SpaceId | undefined,
343
+ ): Layer.Layer<OperationRegistry.Service> =>
344
+ Layer.succeed(OperationRegistry.Service, {
345
+ resolve: (key: string) =>
346
+ Effect.gen(function* () {
347
+ const records = yield* Effect.tryPromise(() => functionsService.query({ spaceId })).pipe(Effect.orDie);
348
+ const match = (records as Operation.PersistentOperation[]).find((record) => record.key === key);
349
+ return match ? Option.some(Operation.deserialize(match)) : Option.none();
350
+ }),
351
+ });
352
+
353
+ const emptyOperationRegistryLayer = Layer.succeed(OperationRegistry.Service, {
354
+ resolve: () => Effect.succeed(Option.none()),
355
+ });
356
+
357
+ const decodeRefsFromSchema = (ast: SchemaAST.AST, value: unknown, db: EchoDatabaseImpl): unknown => {
358
+ if (value == null) {
359
+ return value;
360
+ }
361
+
362
+ const encoded = SchemaAST.encodedBoundAST(ast);
363
+ if (Ref.isRefType(encoded)) {
364
+ if (Ref.isRef(value)) {
365
+ return value;
366
+ }
367
+
368
+ if (typeof value === 'object' && value !== null && typeof (value as any)['/'] === 'string') {
369
+ const resolver = db.graph.createRefResolver({ context: { space: db.spaceId } });
370
+ return refFromEncodedReference(value as any, resolver);
371
+ }
372
+
373
+ return value;
374
+ }
375
+
376
+ switch (encoded._tag) {
377
+ case 'TypeLiteral': {
378
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
379
+ return value;
380
+ }
381
+ const result: Record<string, unknown> = { ...(value as any) };
382
+ for (const prop of SchemaAST.getPropertySignatures(encoded)) {
383
+ const key = prop.name.toString();
384
+ if (key in result) {
385
+ result[key] = decodeRefsFromSchema(prop.type, (result as any)[key], db);
386
+ }
387
+ }
388
+ return result;
389
+ }
390
+
391
+ case 'TupleType': {
392
+ if (!Array.isArray(value)) {
393
+ return value;
394
+ }
395
+
396
+ // For arrays, effect uses TupleType with empty elements and a single rest element.
397
+ if (encoded.elements.length === 0 && encoded.rest.length === 1) {
398
+ const elementType = encoded.rest[0].type;
399
+ return (value as unknown[]).map((item) => decodeRefsFromSchema(elementType, item, db));
400
+ }
401
+
402
+ return value;
403
+ }
404
+
405
+ case 'Union': {
406
+ // Optional values are represented as union with undefined.
407
+ const nonUndefined = encoded.types.filter((t) => !SchemaAST.isUndefinedKeyword(t));
408
+ if (nonUndefined.length === 1) {
409
+ return decodeRefsFromSchema(nonUndefined[0], value, db);
410
+ }
411
+
412
+ // For other unions we can't safely pick a branch without validating.
413
+ return value;
414
+ }
415
+
416
+ case 'Suspend': {
417
+ return decodeRefsFromSchema(encoded.f(), value, db);
418
+ }
419
+
420
+ case 'Refinement': {
421
+ return decodeRefsFromSchema(encoded.from, value, db);
422
+ }
423
+
424
+ default: {
425
+ return value;
426
+ }
427
+ }
428
+ };
package/src/sdk.ts ADDED
@@ -0,0 +1,31 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { type AiService } from '@dxos/ai';
6
+ import { type Credential, type Operation, type Trace } from '@dxos/compute';
7
+ import { type Database, type Feed } from '@dxos/echo';
8
+
9
+ import { type FunctionInvocationService, type QueueService } from './services';
10
+
11
+ // TODO(burdon): Model after http request. Ref Lambda/OpenFaaS.
12
+ // https://docs.aws.amazon.com/lambda/latest/dg/typescript-handler.html
13
+ // https://www.serverless.com/framework/docs/providers/aws/guide/serverless.yml/#functions
14
+ // https://www.npmjs.com/package/aws-lambda
15
+
16
+ /**
17
+ * Services that are available to invoked functions.
18
+ * @deprecated
19
+ */
20
+ export type FunctionServices =
21
+ | AiService.AiService
22
+ | Credential.CredentialsService
23
+ | Database.Service
24
+ // TODO(wittjosiah): Remove QueueService — use Feed.FeedService instead.
25
+ | QueueService
26
+ | Feed.FeedService
27
+ | Trace.TraceService
28
+ // TODO(dmaretskyi): `FunctionInvocationService` is being phased out in favour of `Operation.Service`;
29
+ // it's kept in the union until `functions-runtime/local-function-execution.ts` migrates.
30
+ | FunctionInvocationService
31
+ | Operation.Service;
@@ -2,136 +2,115 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { HttpClient, HttpClientRequest } from '@effect/platform';
6
- import { type Config, Context, Effect, Layer, Redacted } from 'effect';
7
-
5
+ import * as HttpClient from '@effect/platform/HttpClient';
6
+ import * as HttpClientRequest from '@effect/platform/HttpClientRequest';
7
+ import type * as Config from 'effect/Config';
8
+ import * as Context from 'effect/Context';
9
+ import * as Effect from 'effect/Effect';
10
+ import * as Layer from 'effect/Layer';
11
+ import * as Redacted from 'effect/Redacted';
12
+
13
+ import { Credential } from '@dxos/compute';
8
14
  import { Query } from '@dxos/echo';
9
- import { DataType } from '@dxos/schema';
10
-
11
- import { DatabaseService } from './database';
12
-
13
- export type CredentialQuery = {
14
- service?: string;
15
- };
16
-
17
- // TODO(dmaretskyi): Unify with other apis.
18
- // packages/sdk/schema/src/common/access-token.ts
19
- export type ServiceCredential = {
20
- service: string;
21
-
22
- // TODO(dmaretskyi): Build out.
23
- apiKey?: string;
24
- };
25
-
26
- export class CredentialsService extends Context.Tag('@dxos/functions/CredentialsService')<
27
- CredentialsService,
28
- {
29
- /**
30
- * Query all.
31
- */
32
- queryCredentials: (query: CredentialQuery) => Promise<ServiceCredential[]>;
33
-
34
- /**
35
- * Get a single credential.
36
- * @throws {Error} If no credential is found.
37
- */
38
- getCredential: (query: CredentialQuery) => Promise<ServiceCredential>;
39
- }
40
- >() {
41
- static getCredential = (query: CredentialQuery): Effect.Effect<ServiceCredential, never, CredentialsService> =>
42
- Effect.gen(function* () {
43
- const credentials = yield* CredentialsService;
44
- return yield* Effect.promise(() => credentials.getCredential(query));
45
- });
46
-
47
- static getApiKey = (query: CredentialQuery): Effect.Effect<Redacted.Redacted<string>, never, CredentialsService> =>
48
- Effect.gen(function* () {
49
- const credential = yield* CredentialsService.getCredential(query);
50
- if (!credential.apiKey) {
51
- throw new Error(`API key not found for service: ${query.service}`);
52
- }
53
- return Redacted.make(credential.apiKey);
54
- });
55
-
56
- static configuredLayer = (credentials: ServiceCredential[]) =>
57
- Layer.succeed(CredentialsService, new ConfiguredCredentialsService(credentials));
58
-
59
- static layerConfig = (credentials: { service: string; apiKey: Config.Config<Redacted.Redacted<string>> }[]) =>
60
- Layer.effect(
61
- CredentialsService,
62
- Effect.gen(function* () {
63
- const serviceCredentials = yield* Effect.forEach(credentials, ({ service, apiKey }) =>
64
- Effect.gen(function* () {
65
- return {
66
- service,
67
- apiKey: Redacted.value(yield* apiKey),
68
- };
69
- }),
70
- );
71
-
72
- return new ConfiguredCredentialsService(serviceCredentials);
73
- }),
74
- );
75
-
76
- static layerFromDatabase = () =>
77
- Layer.effect(
78
- CredentialsService,
79
- Effect.gen(function* () {
80
- const dbService = yield* DatabaseService;
81
- const queryCredentials = async (query: CredentialQuery): Promise<ServiceCredential[]> => {
82
- const { objects: accessTokens } = await dbService.db.query(Query.type(DataType.AccessToken)).run();
83
- return accessTokens
84
- .filter((accessToken) => accessToken.source === query.service)
85
- .map((accessToken) => ({
86
- service: accessToken.source,
87
- apiKey: accessToken.token,
88
- }));
89
- };
90
- return {
91
- getCredential: async (query) => {
92
- const credentials = await queryCredentials(query);
93
- if (credentials.length === 0) {
94
- throw new Error(`Credential not found for service: ${query.service}`);
95
- }
96
- return credentials[0];
97
- },
98
- queryCredentials: async (query) => {
99
- return queryCredentials(query);
100
- },
101
- };
102
- }),
103
- );
104
- }
15
+ import { Database } from '@dxos/echo';
16
+ import { AccessToken } from '@dxos/types';
105
17
 
106
- export class ConfiguredCredentialsService implements Context.Tag.Service<CredentialsService> {
107
- constructor(private readonly credentials: ServiceCredential[] = []) {}
18
+ export class ConfiguredCredentialsService implements Context.Tag.Service<Credential.CredentialsService> {
19
+ constructor(private readonly credentials: Credential.ServiceCredential[] = []) {}
108
20
 
109
- addCredentials(credentials: ServiceCredential[]): ConfiguredCredentialsService {
21
+ addCredentials(credentials: Credential.ServiceCredential[]): ConfiguredCredentialsService {
110
22
  this.credentials.push(...credentials);
111
23
  return this;
112
24
  }
113
25
 
114
- async queryCredentials(query: CredentialQuery): Promise<ServiceCredential[]> {
26
+ async queryCredentials(query: Credential.CredentialQuery): Promise<Credential.ServiceCredential[]> {
115
27
  return this.credentials.filter((credential) => credential.service === query.service);
116
28
  }
117
29
 
118
- async getCredential(query: CredentialQuery): Promise<ServiceCredential> {
30
+ async getCredential(query: Credential.CredentialQuery): Promise<Credential.ServiceCredential> {
119
31
  const credential = this.credentials.find((credential) => credential.service === query.service);
120
32
  if (!credential) {
121
33
  throw new Error(`Credential not found for service: ${query.service}`);
122
34
  }
35
+
123
36
  return credential;
124
37
  }
125
38
  }
126
39
 
127
40
  /**
128
- * Maps the request to include the API key from the credential.
41
+ * Maps the request to include the given token in the Authorization header.
129
42
  */
130
- export const withAuthorization = (query: CredentialQuery, kind?: 'Bearer' | 'Basic') =>
131
- HttpClient.mapRequestEffect(
132
- Effect.fnUntraced(function* (request) {
133
- const key = yield* CredentialsService.getApiKey(query).pipe(Effect.map(Redacted.value));
134
- const authorization = kind ? `${kind} ${key}` : key;
135
- return HttpClientRequest.setHeader(request, 'Authorization', authorization);
43
+ export const withAuthorization = (token: string, kind?: 'Bearer' | 'Basic') =>
44
+ HttpClient.mapRequest((request) => {
45
+ const authorization = kind ? `${kind} ${token}` : token;
46
+ return HttpClientRequest.setHeader(request, 'Authorization', authorization);
47
+ });
48
+
49
+ export const configuredCredentialsLayer = (credentials: Credential.ServiceCredential[]) =>
50
+ Layer.succeed(Credential.CredentialsService, new ConfiguredCredentialsService(credentials));
51
+
52
+ export const credentialsLayerConfig = (
53
+ credentials: {
54
+ service: string;
55
+ apiKey: Config.Config<Redacted.Redacted<string>>;
56
+ }[],
57
+ ) =>
58
+ Layer.effect(
59
+ Credential.CredentialsService,
60
+ Effect.gen(function* () {
61
+ const serviceCredentials = yield* Effect.forEach(credentials, ({ service, apiKey }) =>
62
+ Effect.gen(function* () {
63
+ return {
64
+ service,
65
+ apiKey: Redacted.value(yield* apiKey),
66
+ };
67
+ }),
68
+ );
69
+
70
+ return new ConfiguredCredentialsService(serviceCredentials);
71
+ }),
72
+ );
73
+
74
+ export const credentialsLayerFromDatabase = ({ caching = false }: { caching?: boolean } = {}) =>
75
+ Layer.effect(
76
+ Credential.CredentialsService,
77
+ Effect.gen(function* () {
78
+ const dbService = yield* Database.Service;
79
+ const cache = new Map<string, Credential.ServiceCredential[]>();
80
+
81
+ const queryCredentials = async (query: Credential.CredentialQuery): Promise<Credential.ServiceCredential[]> => {
82
+ const cacheKey = JSON.stringify(query);
83
+ if (caching && cache.has(cacheKey)) {
84
+ return cache.get(cacheKey)!;
85
+ }
86
+
87
+ const accessTokens = await dbService.db.query(Query.type(AccessToken.AccessToken)).run();
88
+ const credentials = accessTokens
89
+ .filter((accessToken) => accessToken.source === query.service)
90
+ .map((accessToken) => ({
91
+ service: accessToken.source,
92
+ apiKey: accessToken.token,
93
+ }));
94
+
95
+ if (caching) {
96
+ cache.set(cacheKey, credentials);
97
+ }
98
+
99
+ return credentials;
100
+ };
101
+
102
+ return {
103
+ getCredential: async (query) => {
104
+ const credentials = await queryCredentials(query);
105
+ if (credentials.length === 0) {
106
+ throw new Error(`Credential not found for service: ${query.service}`);
107
+ }
108
+
109
+ return credentials[0];
110
+ },
111
+ queryCredentials: async (query) => {
112
+ return queryCredentials(query);
113
+ },
114
+ };
136
115
  }),
137
116
  );