@dxos/compute 0.8.4-main.fd6878d → 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.
- package/LICENSE +102 -5
- package/README.md +36 -1
- package/dist/lib/neutral/StorageService.mjs +22 -0
- package/dist/lib/neutral/chunk-2B2KSJT6.mjs +17 -0
- package/dist/lib/neutral/chunk-2B2KSJT6.mjs.map +7 -0
- package/dist/lib/neutral/chunk-AFBNVFAY.mjs +25 -0
- package/dist/lib/neutral/chunk-AFBNVFAY.mjs.map +7 -0
- package/dist/lib/neutral/chunk-J5LGTIGS.mjs +10 -0
- package/dist/lib/neutral/chunk-JAU4E4EK.mjs +53 -0
- package/dist/lib/neutral/chunk-JAU4E4EK.mjs.map +7 -0
- package/dist/lib/neutral/chunk-MEPE4HHX.mjs +21 -0
- package/dist/lib/neutral/chunk-MEPE4HHX.mjs.map +7 -0
- package/dist/lib/neutral/chunk-QZS4G6PE.mjs +240 -0
- package/dist/lib/neutral/chunk-QZS4G6PE.mjs.map +7 -0
- package/dist/lib/neutral/chunk-XEVFBI6S.mjs +62 -0
- package/dist/lib/neutral/chunk-XEVFBI6S.mjs.map +7 -0
- package/dist/lib/neutral/chunk-Z3Y7FQGN.mjs +174 -0
- package/dist/lib/neutral/chunk-Z3Y7FQGN.mjs.map +7 -0
- package/dist/lib/neutral/fib-HBDUGQW7.mjs +10 -0
- package/dist/lib/neutral/fib-HBDUGQW7.mjs.map +7 -0
- package/dist/lib/neutral/index.mjs +1183 -0
- package/dist/lib/neutral/index.mjs.map +7 -0
- package/dist/lib/neutral/meta.json +1 -0
- package/dist/lib/neutral/reply-RJIAJ6FV.mjs +10 -0
- package/dist/lib/neutral/reply-RJIAJ6FV.mjs.map +7 -0
- package/dist/lib/neutral/sleep-3T2CZ7RE.mjs +10 -0
- package/dist/lib/neutral/sleep-3T2CZ7RE.mjs.map +7 -0
- package/dist/lib/neutral/testing/index.mjs +32 -0
- package/dist/lib/neutral/testing/index.mjs.map +7 -0
- package/dist/types/src/Blueprint.d.ts +182 -0
- package/dist/types/src/Blueprint.d.ts.map +1 -0
- package/dist/types/src/Credential.d.ts +27 -0
- package/dist/types/src/Credential.d.ts.map +1 -0
- package/dist/types/src/Header.d.ts +9 -0
- package/dist/types/src/Header.d.ts.map +1 -0
- package/dist/types/src/Header.test.d.ts +2 -0
- package/dist/types/src/Header.test.d.ts.map +1 -0
- package/dist/types/src/LayerSpec.d.ts +56 -0
- package/dist/types/src/LayerSpec.d.ts.map +1 -0
- package/dist/types/src/McpServer.d.ts +26 -0
- package/dist/types/src/McpServer.d.ts.map +1 -0
- package/dist/types/src/Operation.d.ts +385 -0
- package/dist/types/src/Operation.d.ts.map +1 -0
- package/dist/types/src/OperationHandlerSet.d.ts +69 -0
- package/dist/types/src/OperationHandlerSet.d.ts.map +1 -0
- package/dist/types/src/OperationHandlerSet.test.d.ts +2 -0
- package/dist/types/src/OperationHandlerSet.test.d.ts.map +1 -0
- package/dist/types/src/OperationRegistry.d.ts +23 -0
- package/dist/types/src/OperationRegistry.d.ts.map +1 -0
- package/dist/types/src/Process.d.ts +255 -0
- package/dist/types/src/Process.d.ts.map +1 -0
- package/dist/types/src/Routine.d.ts +54 -0
- package/dist/types/src/Routine.d.ts.map +1 -0
- package/dist/types/src/Script.d.ts +21 -0
- package/dist/types/src/Script.d.ts.map +1 -0
- package/dist/types/src/ServiceResolver.d.ts +92 -0
- package/dist/types/src/ServiceResolver.d.ts.map +1 -0
- package/dist/types/src/StorageService.d.ts +58 -0
- package/dist/types/src/StorageService.d.ts.map +1 -0
- package/dist/types/src/Template.d.ts +61 -0
- package/dist/types/src/Template.d.ts.map +1 -0
- package/dist/types/src/Template.test.d.ts +2 -0
- package/dist/types/src/Template.test.d.ts.map +1 -0
- package/dist/types/src/Trace.d.ts +266 -0
- package/dist/types/src/Trace.d.ts.map +1 -0
- package/dist/types/src/Trigger.d.ts +192 -0
- package/dist/types/src/Trigger.d.ts.map +1 -0
- package/dist/types/src/TriggerEvent.d.ts +83 -0
- package/dist/types/src/TriggerEvent.d.ts.map +1 -0
- package/dist/types/src/errors.d.ts +308 -0
- package/dist/types/src/errors.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +18 -5
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/testing/definitions.d.ts +11 -0
- package/dist/types/src/testing/definitions.d.ts.map +1 -0
- package/dist/types/src/testing/fib.d.ts +8 -0
- package/dist/types/src/testing/fib.d.ts.map +1 -0
- package/dist/types/src/testing/index.d.ts +6 -3
- package/dist/types/src/testing/index.d.ts.map +1 -1
- package/dist/types/src/testing/reply.d.ts +4 -0
- package/dist/types/src/testing/reply.d.ts.map +1 -0
- package/dist/types/src/testing/sleep.d.ts +6 -0
- package/dist/types/src/testing/sleep.d.ts.map +1 -0
- package/dist/types/src/umbrella.test.d.ts +2 -0
- package/dist/types/src/umbrella.test.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +31 -47
- package/src/Blueprint.ts +224 -0
- package/src/Credential.ts +53 -0
- package/src/Header.test.ts +132 -0
- package/src/Header.ts +38 -0
- package/src/LayerSpec.ts +91 -0
- package/src/McpServer.ts +44 -0
- package/src/Operation.ts +646 -0
- package/src/OperationHandlerSet.test.ts +97 -0
- package/src/OperationHandlerSet.ts +181 -0
- package/src/OperationRegistry.ts +50 -0
- package/src/Process.ts +541 -0
- package/src/Routine.ts +68 -0
- package/src/Script.ts +33 -0
- package/src/ServiceResolver.ts +200 -0
- package/src/StorageService.ts +101 -0
- package/src/Template.test.ts +164 -0
- package/src/Template.ts +125 -0
- package/src/Trace.ts +363 -0
- package/src/Trigger.ts +196 -0
- package/src/TriggerEvent.ts +64 -0
- package/src/errors.ts +71 -0
- package/src/index.ts +19 -32
- package/src/testing/definitions.ts +51 -0
- package/src/testing/fib.ts +22 -0
- package/src/testing/index.ts +13 -4
- package/src/testing/reply.ts +18 -0
- package/src/testing/sleep.ts +16 -0
- package/src/umbrella.test.ts +51 -0
- package/dist/lib/browser/chunk-X2FWMHD4.mjs +0 -3107
- package/dist/lib/browser/chunk-X2FWMHD4.mjs.map +0 -7
- package/dist/lib/browser/index.mjs +0 -55
- package/dist/lib/browser/meta.json +0 -1
- package/dist/lib/browser/testing/index.mjs +0 -176
- package/dist/lib/browser/testing/index.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-RP7HSSCA.mjs +0 -3109
- package/dist/lib/node-esm/chunk-RP7HSSCA.mjs.map +0 -7
- package/dist/lib/node-esm/index.mjs +0 -56
- package/dist/lib/node-esm/meta.json +0 -1
- package/dist/lib/node-esm/testing/index.mjs +0 -177
- package/dist/lib/node-esm/testing/index.mjs.map +0 -7
- package/dist/types/src/compute-graph-registry.d.ts +0 -34
- package/dist/types/src/compute-graph-registry.d.ts.map +0 -1
- package/dist/types/src/compute-graph.d.ts +0 -64
- package/dist/types/src/compute-graph.d.ts.map +0 -1
- package/dist/types/src/compute-graph.test.d.ts +0 -2
- package/dist/types/src/compute-graph.test.d.ts.map +0 -1
- package/dist/types/src/compute-node.d.ts +0 -25
- package/dist/types/src/compute-node.d.ts.map +0 -1
- package/dist/types/src/functions/async-function.d.ts +0 -66
- package/dist/types/src/functions/async-function.d.ts.map +0 -1
- package/dist/types/src/functions/edge-function.d.ts +0 -20
- package/dist/types/src/functions/edge-function.d.ts.map +0 -1
- package/dist/types/src/functions/function-defs.d.ts +0 -11
- package/dist/types/src/functions/function-defs.d.ts.map +0 -1
- package/dist/types/src/functions/index.d.ts +0 -4
- package/dist/types/src/functions/index.d.ts.map +0 -1
- package/dist/types/src/hyperformula.test.d.ts +0 -2
- package/dist/types/src/hyperformula.test.d.ts.map +0 -1
- package/dist/types/src/testing/test-builder.d.ts +0 -15
- package/dist/types/src/testing/test-builder.d.ts.map +0 -1
- package/dist/types/src/testing/test-plugin.d.ts +0 -36
- package/dist/types/src/testing/test-plugin.d.ts.map +0 -1
- package/dist/types/src/testing/util.d.ts +0 -2
- package/dist/types/src/testing/util.d.ts.map +0 -1
- package/dist/types/src/types/hyperformula.d.ts +0 -11
- package/dist/types/src/types/hyperformula.d.ts.map +0 -1
- package/dist/types/src/types/index.d.ts +0 -3
- package/dist/types/src/types/index.d.ts.map +0 -1
- package/dist/types/src/types/types.d.ts +0 -20
- package/dist/types/src/types/types.d.ts.map +0 -1
- package/dist/types/src/types/types.test.d.ts +0 -2
- package/dist/types/src/types/types.test.d.ts.map +0 -1
- package/dist/types/src/util.d.ts +0 -2
- package/dist/types/src/util.d.ts.map +0 -1
- package/dist/vendor/hyperformula.mjs +0 -36400
- package/src/compute-graph-registry.ts +0 -91
- package/src/compute-graph.test.ts +0 -89
- package/src/compute-graph.ts +0 -260
- package/src/compute-node.ts +0 -62
- package/src/functions/async-function.ts +0 -179
- package/src/functions/edge-function.ts +0 -112
- package/src/functions/function-defs.ts +0 -2427
- package/src/functions/index.ts +0 -7
- package/src/hyperformula.test.ts +0 -14
- package/src/testing/test-builder.ts +0 -54
- package/src/testing/test-plugin.ts +0 -101
- package/src/testing/util.ts +0 -9
- package/src/types/hyperformula.ts +0 -19
- package/src/types/index.ts +0 -6
- package/src/types/types.test.ts +0 -91
- package/src/types/types.ts +0 -80
- package/src/util.ts +0 -8
- /package/dist/lib/{browser/index.mjs.map → neutral/StorageService.mjs.map} +0 -0
- /package/dist/lib/{node-esm/index.mjs.map → neutral/chunk-J5LGTIGS.mjs.map} +0 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
// @import-as-namespace
|
|
6
|
+
|
|
7
|
+
import * as Context from 'effect/Context';
|
|
8
|
+
import * as Effect from 'effect/Effect';
|
|
9
|
+
import * as Either from 'effect/Either';
|
|
10
|
+
import * as Layer from 'effect/Layer';
|
|
11
|
+
import * as Option from 'effect/Option';
|
|
12
|
+
import * as Scope from 'effect/Scope';
|
|
13
|
+
|
|
14
|
+
import type { SpaceId, URI } from '@dxos/keys';
|
|
15
|
+
|
|
16
|
+
import { ServiceNotAvailableError } from './errors';
|
|
17
|
+
import * as Process from './Process';
|
|
18
|
+
|
|
19
|
+
const ServiceResolverTypeId = '~@dxos/functions/ServiceResolver' as const;
|
|
20
|
+
type ServiceResolverTypeId = typeof ServiceResolverTypeId;
|
|
21
|
+
|
|
22
|
+
export interface ServiceResolver {
|
|
23
|
+
readonly [ServiceResolverTypeId]: ServiceResolverTypeId;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolve a set of services identified by their tags.
|
|
27
|
+
* Returns a Context containing all requested services, or fails with ServiceNotAvailableError.
|
|
28
|
+
*/
|
|
29
|
+
resolve<Tag extends Context.Tag<any, any>>(
|
|
30
|
+
tag: Tag,
|
|
31
|
+
context: ResolutionContext,
|
|
32
|
+
): Effect.Effect<Context.Tag.Service<Tag>, ServiceNotAvailableError, Scope.Scope>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Tag for the ServiceResolver service.
|
|
37
|
+
*/
|
|
38
|
+
export const ServiceResolver = Context.GenericTag<ServiceResolver>('@dxos/functions/ServiceResolver');
|
|
39
|
+
|
|
40
|
+
export const resolve: {
|
|
41
|
+
<Tag extends Context.Tag<any, any>>(
|
|
42
|
+
tag: Tag,
|
|
43
|
+
context: ResolutionContext,
|
|
44
|
+
): Effect.Effect<Context.Tag.Service<Tag>, ServiceNotAvailableError, Scope.Scope | ServiceResolver>;
|
|
45
|
+
} = Effect.serviceFunctionEffect(ServiceResolver, (_) => _.resolve);
|
|
46
|
+
|
|
47
|
+
export const resolveAll = <const Tags extends readonly Context.Tag<any, any>[]>(
|
|
48
|
+
tags: Tags,
|
|
49
|
+
context: ResolutionContext,
|
|
50
|
+
): Effect.Effect<Context.Context<Tags[number]>, ServiceNotAvailableError, Scope.Scope | ServiceResolver> =>
|
|
51
|
+
Effect.gen(function* () {
|
|
52
|
+
const services = yield* Effect.forEach(tags, (tag) =>
|
|
53
|
+
resolve(tag, context).pipe(Effect.map((service) => Context.make(tag, service))),
|
|
54
|
+
);
|
|
55
|
+
return Context.mergeAll(...services);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Provides context for service resolution.
|
|
60
|
+
*/
|
|
61
|
+
export interface ResolutionContext {
|
|
62
|
+
/**
|
|
63
|
+
* Under which identity the process is running.
|
|
64
|
+
*/
|
|
65
|
+
readonly identity?: string;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Under which space the process is running.
|
|
69
|
+
*/
|
|
70
|
+
readonly space?: SpaceId;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* URI of the conversation feed the process is running in. Today always an EID;
|
|
74
|
+
* narrow with `EID.parse` at the point of use.
|
|
75
|
+
*/
|
|
76
|
+
readonly conversation?: URI.URI;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Under which process the process is running.
|
|
80
|
+
*/
|
|
81
|
+
readonly process?: Process.ID;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const succeed = <I, S>(
|
|
85
|
+
tag: Context.Tag<I, S>,
|
|
86
|
+
getService: (context: ResolutionContext) => Effect.Effect<S, ServiceNotAvailableError, Scope.Scope>,
|
|
87
|
+
): ServiceResolver => {
|
|
88
|
+
return make((tag1, context) => {
|
|
89
|
+
if (tag1.key !== tag.key) {
|
|
90
|
+
return Effect.fail(new ServiceNotAvailableError(`Service not available: ${String(tag.key ?? tag)}`));
|
|
91
|
+
}
|
|
92
|
+
const service = getService(context);
|
|
93
|
+
return service as any;
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Create a ServiceResolver from a custom resolution function.
|
|
99
|
+
*/
|
|
100
|
+
export const make = (
|
|
101
|
+
resolveFn: <I, S>(
|
|
102
|
+
tag: Context.Tag<I, S>,
|
|
103
|
+
context: ResolutionContext,
|
|
104
|
+
) => Effect.Effect<S, ServiceNotAvailableError, Scope.Scope>,
|
|
105
|
+
): ServiceResolver => ({
|
|
106
|
+
[ServiceResolverTypeId]: ServiceResolverTypeId,
|
|
107
|
+
resolve: resolveFn,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Create a ServiceResolver backed by a static Context.
|
|
112
|
+
* Tags present in the context are resolved; missing tags fail with ServiceNotAvailableError.
|
|
113
|
+
*/
|
|
114
|
+
export const fromContext = <Services>(ctx: Context.Context<Services>): ServiceResolver =>
|
|
115
|
+
make((tag, context) =>
|
|
116
|
+
Effect.gen(function* () {
|
|
117
|
+
const service = Context.getOption(ctx, tag);
|
|
118
|
+
if (Option.isNone(service)) {
|
|
119
|
+
return yield* Effect.fail(new ServiceNotAvailableError(String(tag.key ?? tag)));
|
|
120
|
+
}
|
|
121
|
+
return service.value;
|
|
122
|
+
}),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Create a ServiceResolver that resolves tags from the current Effect context.
|
|
127
|
+
* Only the specified tags are available; requests for other tags fail.
|
|
128
|
+
*/
|
|
129
|
+
export const fromRequirements = <const Tags extends readonly Context.Tag<any, any>[]>(
|
|
130
|
+
...tags: Tags
|
|
131
|
+
): Effect.Effect<ServiceResolver, never, Context.Tag.Identifier<Tags[number]>> =>
|
|
132
|
+
Effect.contextWith((parentCtx: Context.Context<any>) => {
|
|
133
|
+
const available = new Set(tags.map((tag) => tag.key));
|
|
134
|
+
return make((tag, context) =>
|
|
135
|
+
Effect.gen(function* () {
|
|
136
|
+
let result: Context.Context<never> = Context.empty() as Context.Context<never>;
|
|
137
|
+
if (!available.has(tag.key)) {
|
|
138
|
+
return yield* Effect.fail(new ServiceNotAvailableError(String(tag.key ?? tag)));
|
|
139
|
+
}
|
|
140
|
+
const service = Context.getOption(parentCtx, tag);
|
|
141
|
+
if (Option.isNone(service)) {
|
|
142
|
+
return yield* Effect.fail(new ServiceNotAvailableError(String(tag.key ?? tag)));
|
|
143
|
+
}
|
|
144
|
+
return service.value;
|
|
145
|
+
}),
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Like {@link fromRequirements} but returns a Layer that provides ServiceResolver.
|
|
151
|
+
*/
|
|
152
|
+
export const layerRequirements = <const Tags extends readonly Context.Tag<any, any>[]>(
|
|
153
|
+
...tags: Tags
|
|
154
|
+
): Layer.Layer<ServiceResolver, never, Context.Tag.Identifier<Tags[number]>> =>
|
|
155
|
+
Layer.effect(ServiceResolver, fromRequirements(...tags));
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Build a {@link Layer} that materialises the requested services by calling
|
|
159
|
+
* {@link resolveAll} against the {@link ServiceResolver} currently in context.
|
|
160
|
+
*
|
|
161
|
+
* The returned layer manages its own {@link Scope.Scope} and requires only a
|
|
162
|
+
* {@link ServiceResolver} (typically supplied by the process-manager runtime).
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* ```ts
|
|
166
|
+
* processManagerRuntime.runPromise(
|
|
167
|
+
* myEffect.pipe(Effect.provide(ServiceResolver.provide({ space }, Database.Service, Feed.FeedService))),
|
|
168
|
+
* );
|
|
169
|
+
* ```
|
|
170
|
+
*/
|
|
171
|
+
export const provide = <const Tags extends readonly Context.Tag<any, any>[]>(
|
|
172
|
+
context: ResolutionContext,
|
|
173
|
+
...tags: Tags
|
|
174
|
+
): Layer.Layer<Context.Tag.Identifier<Tags[number]>, ServiceNotAvailableError, ServiceResolver> =>
|
|
175
|
+
Layer.scopedContext(resolveAll(tags, context));
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Compose multiple resolvers left to right. Earlier resolvers take precedence:
|
|
179
|
+
* the first resolver that can satisfy a tag wins.
|
|
180
|
+
*/
|
|
181
|
+
export const compose = (...resolvers: readonly ServiceResolver[]): ServiceResolver =>
|
|
182
|
+
make((tag, context) =>
|
|
183
|
+
Effect.gen(function* () {
|
|
184
|
+
for (const resolver of resolvers) {
|
|
185
|
+
const single = yield* resolver.resolve(tag, context).pipe(Effect.either);
|
|
186
|
+
if (Either.isRight(single)) {
|
|
187
|
+
return single.right;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return yield* Effect.fail(new ServiceNotAvailableError(String(tag.key ?? tag)));
|
|
192
|
+
}),
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* An empty resolver that fails for any requested service.
|
|
197
|
+
*/
|
|
198
|
+
export const empty: ServiceResolver = make((tag, context) => {
|
|
199
|
+
return Effect.fail(new ServiceNotAvailableError(String(tag.key ?? tag)));
|
|
200
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
// @import-as-namespace
|
|
6
|
+
|
|
7
|
+
import * as Context from 'effect/Context';
|
|
8
|
+
import * as Effect from 'effect/Effect';
|
|
9
|
+
import * as Option from 'effect/Option';
|
|
10
|
+
import * as Pipeable from 'effect/Pipeable';
|
|
11
|
+
import * as Schema from 'effect/Schema';
|
|
12
|
+
|
|
13
|
+
export interface Service {
|
|
14
|
+
/** Read a value by key. Returns `None` if key does not exist. */
|
|
15
|
+
get<S extends Schema.Schema<any, string, any>>(
|
|
16
|
+
schema: S,
|
|
17
|
+
key: string,
|
|
18
|
+
): Effect.Effect<Option.Option<Schema.Schema.Type<S>>, never, Schema.Schema.Context<S>>;
|
|
19
|
+
|
|
20
|
+
/** Write a value for the given key. */
|
|
21
|
+
set<S extends Schema.Schema<any, string, any>>(
|
|
22
|
+
schema: S,
|
|
23
|
+
key: string,
|
|
24
|
+
value: Schema.Schema.Type<S>,
|
|
25
|
+
): Effect.Effect<void, never, Schema.Schema.Context<S>>;
|
|
26
|
+
|
|
27
|
+
/** Remove a key. */
|
|
28
|
+
delete(key: string): Effect.Effect<void>;
|
|
29
|
+
|
|
30
|
+
/** List all keys, optionally filtered by prefix. */
|
|
31
|
+
list(prefix?: string): Effect.Effect<readonly string[]>;
|
|
32
|
+
|
|
33
|
+
/** Remove all keys managed by this scoped store. */
|
|
34
|
+
clear(): Effect.Effect<void>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Scoped key-value storage service for processes.
|
|
39
|
+
* Each process receives its own namespaced instance via the process manager.
|
|
40
|
+
* Construct a live implementation with `StorageService.layer` from `@dxos/functions-runtime`.
|
|
41
|
+
*/
|
|
42
|
+
export class StorageService extends Context.Tag('@dxos/functions/StorageService')<StorageService, Service>() {}
|
|
43
|
+
|
|
44
|
+
export const get = Effect.serviceFunctionEffect(StorageService, (_) => _.get);
|
|
45
|
+
export const set = Effect.serviceFunctionEffect(StorageService, (_) => _.set);
|
|
46
|
+
export const deleteKey = Effect.serviceFunctionEffect(StorageService, (_) => _.delete);
|
|
47
|
+
export const list = Effect.serviceFunctionEffect(StorageService, (_) => _.list);
|
|
48
|
+
export const clear = Effect.serviceFunctionEffect(StorageService, (_) => _.clear);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Typed key in a storage service.
|
|
52
|
+
*/
|
|
53
|
+
export interface Key<T> extends Pipeable.Pipeable {
|
|
54
|
+
readonly key: string;
|
|
55
|
+
|
|
56
|
+
get: Effect.Effect<Option.Option<T>, never, StorageService>;
|
|
57
|
+
set(value: T): Effect.Effect<void, never, StorageService>;
|
|
58
|
+
delete(): Effect.Effect<void, never, StorageService>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create a typed key in a storage service.
|
|
63
|
+
*/
|
|
64
|
+
export const key = <S extends Schema.Schema<any, string, any>>(schema: S, key: string): Key<Schema.Schema.Type<S>> => {
|
|
65
|
+
return {
|
|
66
|
+
key,
|
|
67
|
+
get: get(schema, key),
|
|
68
|
+
set: (value: Schema.Schema.Type<S>) => set(schema, key, value),
|
|
69
|
+
delete: () => deleteKey(key),
|
|
70
|
+
pipe(...args: any) {
|
|
71
|
+
return Pipeable.pipeArguments(this, arguments);
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Typed key in a storage service with a default value.
|
|
78
|
+
*/
|
|
79
|
+
export interface KeyWithDefault<T, U> extends Pipeable.Pipeable {
|
|
80
|
+
readonly key: string;
|
|
81
|
+
get: Effect.Effect<T | U, never, StorageService>;
|
|
82
|
+
set(value: U): Effect.Effect<void, never, StorageService>;
|
|
83
|
+
delete(): Effect.Effect<void, never, StorageService>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Assign a default value to a key if it is not present.
|
|
88
|
+
*/
|
|
89
|
+
export const withDefault =
|
|
90
|
+
<T>(getDefault: () => NoInfer<T>) =>
|
|
91
|
+
(key: Key<T>): KeyWithDefault<T, T> => {
|
|
92
|
+
return {
|
|
93
|
+
key: key.key,
|
|
94
|
+
get: key.get.pipe(Effect.map(Option.getOrElse(() => getDefault()))),
|
|
95
|
+
set: (value) => key.set(value),
|
|
96
|
+
delete: () => key.delete(),
|
|
97
|
+
pipe(...args: any) {
|
|
98
|
+
return Pipeable.pipeArguments(this, arguments);
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 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 { registryLayer } from '@dxos/echo-client';
|
|
10
|
+
import { DXN } from '@dxos/keys';
|
|
11
|
+
|
|
12
|
+
import { FunctionNotFoundError } from './errors';
|
|
13
|
+
import * as Operation from './Operation';
|
|
14
|
+
import * as Template from './Template';
|
|
15
|
+
|
|
16
|
+
describe('Template', () => {
|
|
17
|
+
describe('make', () => {
|
|
18
|
+
test('creates a template with defaults', () => {
|
|
19
|
+
const template = Template.make();
|
|
20
|
+
expect(template.source).toBeDefined();
|
|
21
|
+
expect(template.inputs).toEqual([]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('wraps the source string as a Text ref', () => {
|
|
25
|
+
const template = Template.make({ source: 'Hello {{name}}.' });
|
|
26
|
+
expect(template.source.target?.content).toBe('Hello {{name}}.');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('preserves inputs', () => {
|
|
30
|
+
const template = Template.make({
|
|
31
|
+
source: 'Hello {{name}}.',
|
|
32
|
+
inputs: [{ name: 'name', kind: 'value', default: 'world' }],
|
|
33
|
+
});
|
|
34
|
+
expect(template.inputs).toHaveLength(1);
|
|
35
|
+
expect(template.inputs?.[0]).toMatchObject({ name: 'name', kind: 'value', default: 'world' });
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('schema', () => {
|
|
40
|
+
test('Template is a Schema', () => {
|
|
41
|
+
expect(Schema.isSchema(Template.Template)).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('Input is a Schema', () => {
|
|
45
|
+
expect(Schema.isSchema(Template.Input)).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('InputKind accepts all documented kinds', () => {
|
|
49
|
+
const decode = Schema.decodeSync(Template.InputKind);
|
|
50
|
+
for (const kind of ['value', 'operation']) {
|
|
51
|
+
expect(decode(kind as any)).toBe(kind);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('InputKind rejects unknown kinds', () => {
|
|
56
|
+
expect(() => Schema.decodeSync(Template.InputKind)('nonsense' as any)).toThrow();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('process', () => {
|
|
61
|
+
test('substitutes simple variables', () => {
|
|
62
|
+
expect(Template.process('Hello {{name}}.', { name: 'world' })).toBe('Hello world.');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('renders an empty variables object as empty placeholders', () => {
|
|
66
|
+
expect(Template.process('Hello {{name}}.')).toBe('Hello .');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('trims leading and trailing whitespace', () => {
|
|
70
|
+
expect(Template.process(' \n\nHello.\n\n ')).toBe('Hello.');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('collapses runs of 3+ blank lines down to one blank line', () => {
|
|
74
|
+
const input = 'a\n\n\n\n\nb';
|
|
75
|
+
expect(Template.process(input)).toBe('a\n\nb');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('preserves double newlines', () => {
|
|
79
|
+
expect(Template.process('a\n\nb')).toBe('a\n\nb');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('{{section}} helper increments on each call', () => {
|
|
83
|
+
expect(Template.process('{{section}} / {{section}} / {{section}}')).toBe('1 / 2 / 3');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('processTemplate', () => {
|
|
88
|
+
const GREET_KEY = DXN.make('org.example.test.greet');
|
|
89
|
+
|
|
90
|
+
const greet = Operation.withHandler(
|
|
91
|
+
Operation.make({
|
|
92
|
+
input: Schema.Void,
|
|
93
|
+
output: Schema.String,
|
|
94
|
+
meta: { key: GREET_KEY, name: 'greet' },
|
|
95
|
+
}),
|
|
96
|
+
() => Effect.succeed('Alice'),
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// Handler map: full DXN key → invocable function.
|
|
100
|
+
const handlersByKey: Record<string, (input: any) => Effect.Effect<any, any, any>> = {
|
|
101
|
+
[GREET_KEY]: (input) => greet.handler(input),
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const stubInvoker = Effect.provideService(Operation.Service, {
|
|
105
|
+
invoke: (op: any, input: any) => {
|
|
106
|
+
const key = String(op.meta.key);
|
|
107
|
+
const handler = handlersByKey[key];
|
|
108
|
+
return handler ? handler(input) : Effect.die(`no handler for key: ${key}`);
|
|
109
|
+
},
|
|
110
|
+
schedule: () => Effect.succeed(undefined),
|
|
111
|
+
invokePromise: () => Promise.resolve({}),
|
|
112
|
+
// Operation.OperationService.invoke is a complex overloaded type; a partial test stub
|
|
113
|
+
// cannot express all overload variants without the cast.
|
|
114
|
+
} as unknown as Operation.OperationService);
|
|
115
|
+
|
|
116
|
+
test('resolves a value-kind input from its default', async () => {
|
|
117
|
+
const template = Template.make({
|
|
118
|
+
source: 'Hello {{name}}!',
|
|
119
|
+
inputs: [{ name: 'name', kind: 'value', default: 'world' }],
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const result = await Template.processTemplate(template).pipe(
|
|
123
|
+
Effect.provide(registryLayer()),
|
|
124
|
+
stubInvoker,
|
|
125
|
+
Effect.runPromise,
|
|
126
|
+
);
|
|
127
|
+
expect(result).toBe('Hello world!');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('resolves an operation-kind input and substitutes the result', async () => {
|
|
131
|
+
const template = Template.make({
|
|
132
|
+
source: 'Hello {{name}}.',
|
|
133
|
+
inputs: [{ name: 'name', kind: 'operation', operation: GREET_KEY }],
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const result = await Template.processTemplate(template).pipe(
|
|
137
|
+
Effect.provide(registryLayer({ initial: [Operation.serialize(greet)] })),
|
|
138
|
+
stubInvoker,
|
|
139
|
+
Effect.runPromise,
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
expect(result).toBe('Hello Alice.');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('fails with FunctionNotFoundError when the function key cannot be resolved', async () => {
|
|
146
|
+
const template = Template.make({
|
|
147
|
+
source: 'Hello {{name}}.',
|
|
148
|
+
inputs: [{ name: 'name', kind: 'operation', operation: 'test.missing' }],
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const result = await Template.processTemplate(template).pipe(
|
|
152
|
+
Effect.provide(registryLayer()),
|
|
153
|
+
stubInvoker,
|
|
154
|
+
Effect.either,
|
|
155
|
+
Effect.runPromise,
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
expect(result._tag).toBe('Left');
|
|
159
|
+
if (result._tag === 'Left') {
|
|
160
|
+
expect(result.left).toBeInstanceOf(FunctionNotFoundError);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|
package/src/Template.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
// @import-as-namespace
|
|
6
|
+
|
|
7
|
+
import * as Effect from 'effect/Effect';
|
|
8
|
+
import * as Schema from 'effect/Schema';
|
|
9
|
+
|
|
10
|
+
import { Database, DXN, Filter, Ref, Registry } from '@dxos/echo';
|
|
11
|
+
import type { EntityNotFoundError } from '@dxos/echo/Err';
|
|
12
|
+
import { assertArgument, invariant } from '@dxos/invariant';
|
|
13
|
+
import { log } from '@dxos/log';
|
|
14
|
+
import { Text } from '@dxos/schema';
|
|
15
|
+
import Handlebars from '@dxos/vendor-kbn-handlebars';
|
|
16
|
+
|
|
17
|
+
import { FunctionNotFoundError } from './errors';
|
|
18
|
+
import * as Operation from './Operation';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Template input kind determines how template variables are resolved.
|
|
22
|
+
*/
|
|
23
|
+
export const InputKind = Schema.Literal(
|
|
24
|
+
'value', // Literal value.
|
|
25
|
+
'operation',
|
|
26
|
+
// 'pass-through',
|
|
27
|
+
// 'retriever',
|
|
28
|
+
// 'query',
|
|
29
|
+
// 'resolver',
|
|
30
|
+
// 'context',
|
|
31
|
+
// 'schema',
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
export type InputKind = Schema.Schema.Type<typeof InputKind>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Template input variable.
|
|
38
|
+
* E.g., {{foo}}
|
|
39
|
+
*/
|
|
40
|
+
export const Input = Schema.Struct({
|
|
41
|
+
name: Schema.String,
|
|
42
|
+
kind: InputKind,
|
|
43
|
+
default: Schema.optional(Schema.Any),
|
|
44
|
+
/**
|
|
45
|
+
* Operation key invoked when `kind === 'operation'`. The referenced operation must accept void input —
|
|
46
|
+
* template inputs have no mechanism for passing arguments.
|
|
47
|
+
*/
|
|
48
|
+
operation: Schema.optional(Schema.String),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export type Input = Schema.Schema.Type<typeof Input>;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Template type.
|
|
55
|
+
*/
|
|
56
|
+
export const Template = Schema.Struct({
|
|
57
|
+
source: Ref.Ref(Text.Text).annotations({ description: 'Markdown + Handlebars template.' }),
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* NOTE: We use an array rather than map so that updating variable names in the template doesn't disconnect existing inputs.
|
|
61
|
+
*/
|
|
62
|
+
inputs: Schema.optional(Schema.Array(Input)),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export type Template = Schema.Schema.Type<typeof Template>;
|
|
66
|
+
export type MakeProps = Partial<{ id: string; source: string; inputs: Input[] }>;
|
|
67
|
+
|
|
68
|
+
export const make = ({ id, source, inputs = [] }: MakeProps = {}): Template => ({
|
|
69
|
+
source: Ref.make(Text.make({ id, content: source })),
|
|
70
|
+
inputs,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Process Handlebars template.
|
|
75
|
+
*/
|
|
76
|
+
export const process = <Options extends {}>(source: string, variables: Partial<Options> = {}): string => {
|
|
77
|
+
assertArgument(typeof source === 'string', 'source');
|
|
78
|
+
let section = 0;
|
|
79
|
+
const handlebars = Handlebars.create();
|
|
80
|
+
handlebars.registerHelper('section', () => String(++section));
|
|
81
|
+
const template = handlebars.compileAST(source.trim());
|
|
82
|
+
const output = template(variables);
|
|
83
|
+
return output.trim().replace(/(\n\s*){3,}/g, '\n\n');
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const processTemplate = (
|
|
87
|
+
template: Template,
|
|
88
|
+
): Effect.Effect<string, EntityNotFoundError | FunctionNotFoundError, Registry.Service | Operation.Service> =>
|
|
89
|
+
Effect.gen(function* () {
|
|
90
|
+
const entries = yield* Effect.forEach(template.inputs ?? [], (input) =>
|
|
91
|
+
Effect.gen(function* () {
|
|
92
|
+
switch (input.kind) {
|
|
93
|
+
case 'value': {
|
|
94
|
+
return [input.name, input.default] as const;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
case 'operation': {
|
|
98
|
+
invariant(input.operation);
|
|
99
|
+
// Normalize to a full DXN key so Filter.key matches the stored meta key.
|
|
100
|
+
const key = DXN.isDXN(input.operation) ? input.operation : `dxn:${input.operation}`;
|
|
101
|
+
const results = yield* Registry.runQuery(
|
|
102
|
+
Filter.and(Filter.type(Operation.PersistentOperation), Filter.key(key)),
|
|
103
|
+
);
|
|
104
|
+
if (results.length === 0) {
|
|
105
|
+
return yield* Effect.fail(new FunctionNotFoundError(input.operation));
|
|
106
|
+
}
|
|
107
|
+
const fn = Operation.deserialize(results[0]);
|
|
108
|
+
|
|
109
|
+
// NOTE: Operations referenced by template inputs must accept void input — see `Input.operation`.
|
|
110
|
+
const result = yield* Operation.invoke(fn, undefined as any).pipe(Effect.orDie);
|
|
111
|
+
return [input.name, result] as const;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
default: {
|
|
115
|
+
return yield* Effect.dieMessage(`Unsupported input kind: ${input.kind}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const variables = Object.fromEntries(entries);
|
|
122
|
+
|
|
123
|
+
log('processTemplate', { variables });
|
|
124
|
+
return process((yield* Database.load(template.source)).content, variables);
|
|
125
|
+
});
|