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