@dxos/functions 0.8.4-main.72ec0f3 → 0.8.4-main.7ace549

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/functions",
3
- "version": "0.8.4-main.72ec0f3",
3
+ "version": "0.8.4-main.7ace549",
4
4
  "description": "Functions API.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -25,18 +25,19 @@
25
25
  "dependencies": {
26
26
  "@effect/platform": "0.92.1",
27
27
  "effect": "3.18.3",
28
- "@dxos/echo": "0.8.4-main.72ec0f3",
29
- "@dxos/echo-db": "0.8.4-main.72ec0f3",
30
- "@dxos/effect": "0.8.4-main.72ec0f3",
31
- "@dxos/ai": "0.8.4-main.72ec0f3",
32
- "@dxos/errors": "0.8.4-main.72ec0f3",
33
- "@dxos/invariant": "0.8.4-main.72ec0f3",
34
- "@dxos/keys": "0.8.4-main.72ec0f3",
35
- "@dxos/node-std": "0.8.4-main.72ec0f3",
36
- "@dxos/protocols": "0.8.4-main.72ec0f3",
37
- "@dxos/schema": "0.8.4-main.72ec0f3",
38
- "@dxos/types": "0.8.4-main.72ec0f3",
39
- "@dxos/log": "0.8.4-main.72ec0f3"
28
+ "@dxos/ai": "0.8.4-main.7ace549",
29
+ "@dxos/context": "0.8.4-main.7ace549",
30
+ "@dxos/effect": "0.8.4-main.7ace549",
31
+ "@dxos/errors": "0.8.4-main.7ace549",
32
+ "@dxos/echo": "0.8.4-main.7ace549",
33
+ "@dxos/invariant": "0.8.4-main.7ace549",
34
+ "@dxos/echo-db": "0.8.4-main.7ace549",
35
+ "@dxos/log": "0.8.4-main.7ace549",
36
+ "@dxos/node-std": "0.8.4-main.7ace549",
37
+ "@dxos/protocols": "0.8.4-main.7ace549",
38
+ "@dxos/schema": "0.8.4-main.7ace549",
39
+ "@dxos/types": "0.8.4-main.7ace549",
40
+ "@dxos/keys": "0.8.4-main.7ace549"
40
41
  },
41
42
  "publishConfig": {
42
43
  "access": "public"
@@ -8,10 +8,10 @@ import * as Schema from 'effect/Schema';
8
8
  import * as SchemaAST from 'effect/SchemaAST';
9
9
 
10
10
  import { AiService } from '@dxos/ai';
11
+ import { LifecycleState, Resource } from '@dxos/context';
11
12
  import { Type } from '@dxos/echo';
12
- import { EchoClient } from '@dxos/echo-db';
13
- import { acquireReleaseResource } from '@dxos/effect';
14
- import { failedInvariant, invariant } from '@dxos/invariant';
13
+ import { EchoClient, type EchoDatabaseImpl, type QueueFactory } from '@dxos/echo-db';
14
+ import { assertState, failedInvariant, invariant } from '@dxos/invariant';
15
15
  import { PublicKey } from '@dxos/keys';
16
16
  import { type FunctionProtocol } from '@dxos/protocols';
17
17
 
@@ -47,9 +47,21 @@ export const wrapFunctionHandler = (func: FunctionDefinition): FunctionProtocol.
47
47
  });
48
48
  }
49
49
 
50
+ // eslint-disable-next-line no-useless-catch
50
51
  try {
51
52
  if (!SchemaAST.isAnyKeyword(func.inputSchema.ast)) {
52
- Schema.validateSync(func.inputSchema)(data);
53
+ try {
54
+ Schema.validateSync(func.inputSchema)(data);
55
+ } catch (error) {
56
+ throw new FunctionError({ message: 'Invalid input schema', cause: error });
57
+ }
58
+ }
59
+
60
+ await using funcContext = await new FunctionContext(context).open();
61
+
62
+ if (func.types.length > 0) {
63
+ invariant(funcContext.db, 'Database is required for functions with types');
64
+ funcContext.db.graph.schemaRegistry.addSchema(func.types);
53
65
  }
54
66
 
55
67
  let result = await func.handler({
@@ -62,7 +74,7 @@ export const wrapFunctionHandler = (func: FunctionDefinition): FunctionProtocol.
62
74
  result = await Effect.runPromise(
63
75
  (result as Effect.Effect<unknown, unknown, FunctionServices>).pipe(
64
76
  Effect.orDie,
65
- Effect.provide(createServiceLayer(context)),
77
+ Effect.provide(funcContext.createLayer()),
66
78
  ),
67
79
  );
68
80
  }
@@ -73,72 +85,71 @@ export const wrapFunctionHandler = (func: FunctionDefinition): FunctionProtocol.
73
85
 
74
86
  return result;
75
87
  } catch (error) {
76
- if (FunctionError.is(error)) {
77
- throw error;
78
- } else {
79
- throw new FunctionError({
80
- cause: error,
81
- context: { func: func.key },
82
- });
83
- }
88
+ // TODO(dmaretskyi): We might do error wrapping here and add extra context.
89
+ throw error;
84
90
  }
85
91
  },
86
92
  };
87
93
  };
88
94
 
89
95
  /**
90
- * Creates a layer of services for the function.
96
+ * Container for services and context for a function.
91
97
  */
92
- const createServiceLayer = (context: FunctionProtocol.Context): Layer.Layer<FunctionServices> => {
93
- return Layer.unwrapScoped(
94
- Effect.gen(function* () {
95
- let client: EchoClient | undefined;
96
-
97
- if (context.services.dataService && context.services.queryService) {
98
- client = yield* acquireReleaseResource(() => {
99
- invariant(context.services.dataService && context.services.queryService);
100
- // TODO(dmaretskyi): Queues service.
101
- return new EchoClient().connectToService({
102
- dataService: context.services.dataService,
103
- queryService: context.services.queryService,
104
- queueService: context.services.queueService,
105
- });
106
- });
107
- }
98
+ class FunctionContext extends Resource {
99
+ readonly context: FunctionProtocol.Context;
100
+ readonly client: EchoClient | undefined;
101
+ db: EchoDatabaseImpl | undefined;
102
+ queues: QueueFactory | undefined;
103
+
104
+ constructor(context: FunctionProtocol.Context) {
105
+ super();
106
+ this.context = context;
107
+ if (context.services.dataService && context.services.queryService) {
108
+ this.client = new EchoClient().connectToService({
109
+ dataService: context.services.dataService,
110
+ queryService: context.services.queryService,
111
+ queueService: context.services.queueService,
112
+ });
113
+ }
114
+ }
108
115
 
109
- const db =
110
- client && context.spaceId
111
- ? yield* acquireReleaseResource(() =>
112
- client.constructDatabase({
113
- spaceId: context.spaceId ?? failedInvariant(),
114
- spaceKey: PublicKey.fromHex(context.spaceKey ?? failedInvariant('spaceKey missing in context')),
115
- reactiveSchemaQuery: false,
116
- }),
117
- )
118
- : undefined;
119
-
120
- if (db) {
121
- console.log('Setting space root', context.spaceRootUrl);
122
- yield* Effect.promise(() =>
123
- db!.setSpaceRoot(context.spaceRootUrl ?? failedInvariant('spaceRootUrl missing in context')),
124
- );
125
- }
116
+ override async _open() {
117
+ await this.client?.open();
118
+ this.db =
119
+ this.client && this.context.spaceId
120
+ ? this.client.constructDatabase({
121
+ spaceId: this.context.spaceId ?? failedInvariant(),
122
+ spaceKey: PublicKey.fromHex(this.context.spaceKey ?? failedInvariant('spaceKey missing in context')),
123
+ reactiveSchemaQuery: false,
124
+ })
125
+ : undefined;
126
+
127
+ await this.db?.setSpaceRoot(this.context.spaceRootUrl ?? failedInvariant('spaceRootUrl missing in context'));
128
+ await this.db?.open();
129
+ this.queues =
130
+ this.client && this.context.spaceId ? this.client.constructQueueFactory(this.context.spaceId) : undefined;
131
+ }
132
+
133
+ override async _close() {
134
+ await this.db?.close();
135
+ await this.client?.close();
136
+ }
126
137
 
127
- const queues = client && context.spaceId ? client.constructQueueFactory(context.spaceId) : undefined;
138
+ createLayer(): Layer.Layer<FunctionServices> {
139
+ assertState(this._lifecycleState === LifecycleState.OPEN, 'FunctionContext is not open');
128
140
 
129
- const dbLayer = db ? DatabaseService.layer(db) : DatabaseService.notAvailable;
130
- const queuesLayer = queues ? QueueService.layer(queues) : QueueService.notAvailable;
131
- const credentials = dbLayer
132
- ? CredentialsService.layerFromDatabase().pipe(Layer.provide(dbLayer))
133
- : CredentialsService.configuredLayer([]);
134
- const functionInvocationService = MockedFunctionInvocationService;
135
- const aiService = AiService.notAvailable;
136
- const tracing = TracingService.layerNoop;
141
+ const dbLayer = this.db ? DatabaseService.layer(this.db) : DatabaseService.notAvailable;
142
+ const queuesLayer = this.queues ? QueueService.layer(this.queues) : QueueService.notAvailable;
143
+ const credentials = dbLayer
144
+ ? CredentialsService.layerFromDatabase().pipe(Layer.provide(dbLayer))
145
+ : CredentialsService.configuredLayer([]);
146
+ const functionInvocationService = MockedFunctionInvocationService;
147
+ const aiService = AiService.notAvailable;
148
+ const tracing = TracingService.layerNoop;
137
149
 
138
- return Layer.mergeAll(dbLayer, queuesLayer, credentials, functionInvocationService, aiService, tracing);
139
- }),
140
- );
141
- };
150
+ return Layer.mergeAll(dbLayer, queuesLayer, credentials, functionInvocationService, aiService, tracing);
151
+ }
152
+ }
142
153
 
143
154
  const MockedFunctionInvocationService = Layer.succeed(FunctionInvocationService, {
144
155
  invokeFunction: () => Effect.die('Calling functions from functions is not implemented yet.'),
package/src/sdk.ts CHANGED
@@ -75,6 +75,12 @@ export type FunctionDefinition<T = any, O = any, S extends FunctionServices = Fu
75
75
  inputSchema: Schema.Schema<T, any>;
76
76
  outputSchema?: Schema.Schema<O, any>;
77
77
 
78
+ /**
79
+ * List of types the function uses.
80
+ * This is used to ensure that the types are available when the function is executed.
81
+ */
82
+ types: readonly Type.Obj.Any[];
83
+
78
84
  /**
79
85
  * Keys of the required services.
80
86
  */
@@ -108,6 +114,12 @@ export type FunctionProps<T, O> = {
108
114
  description?: string;
109
115
  inputSchema: Schema.Schema<T, any>;
110
116
  outputSchema?: Schema.Schema<O, any>;
117
+
118
+ /**
119
+ * List of types the function uses.
120
+ * This is used to ensure that the types are available when the function is executed.
121
+ */
122
+ types?: readonly Type.Obj.Any[];
111
123
  // TODO(dmaretskyi): This currently doesn't cause a compile-time error if the handler requests a service that is not specified
112
124
  services?: readonly Context.Tag<any, any>[];
113
125
 
@@ -117,7 +129,7 @@ export type FunctionProps<T, O> = {
117
129
  // TODO(dmaretskyi): Output type doesn't get typechecked.
118
130
  export const defineFunction: {
119
131
  <I, O>(params: FunctionProps<I, O>): FunctionDefinition<I, O, FunctionServices>;
120
- } = ({ key, name, description, inputSchema, outputSchema = Schema.Any, handler, services }) => {
132
+ } = ({ key, name, description, inputSchema, outputSchema = Schema.Any, handler, types, services }) => {
121
133
  if (!Schema.isSchema(inputSchema)) {
122
134
  throw new Error('Input schema must be a valid schema');
123
135
  }
@@ -162,6 +174,7 @@ export const defineFunction: {
162
174
  inputSchema,
163
175
  outputSchema,
164
176
  handler: handlerWithSpan,
177
+ types: types ?? [],
165
178
  services: !services ? [] : getServiceKeys(services),
166
179
  } satisfies FunctionDefinition.Any;
167
180
  };
@@ -219,6 +232,7 @@ export const deserializeFunction = (functionObj: Function.Function): FunctionDef
219
232
  // TODO(dmaretskyi): This should throw error.
220
233
  handler: () => {},
221
234
  services: functionObj.services ?? [],
235
+ types: [],
222
236
  meta: {
223
237
  deployedFunctionId: getUserFunctionIdInMetadata(Obj.getMeta(functionObj)),
224
238
  },
@@ -6,7 +6,7 @@ import * as Context from 'effect/Context';
6
6
  import * as Effect from 'effect/Effect';
7
7
  import * as Layer from 'effect/Layer';
8
8
 
9
- import type { Obj, Relation } from '@dxos/echo';
9
+ import type { Entity } from '@dxos/echo';
10
10
  import type { Queue, QueueAPI, QueueFactory } from '@dxos/echo-db';
11
11
  import type { DXN, QueueSubspaceTag } from '@dxos/keys';
12
12
 
@@ -53,22 +53,20 @@ export class QueueService extends Context.Tag('@dxos/functions/QueueService')<
53
53
  /**
54
54
  * Gets a queue by its DXN.
55
55
  */
56
- static getQueue = <T extends Obj.Any | Relation.Any = Obj.Any | Relation.Any>(
56
+ static getQueue = <T extends Entity.Unknown = Entity.Unknown>(
57
57
  dxn: DXN,
58
58
  ): Effect.Effect<Queue<T>, never, QueueService> => QueueService.pipe(Effect.map(({ queues }) => queues.get<T>(dxn)));
59
59
 
60
60
  /**
61
61
  * Creates a new queue.
62
62
  */
63
- static createQueue = <T extends Obj.Any | Relation.Any = Obj.Any | Relation.Any>(options?: {
63
+ static createQueue = <T extends Entity.Unknown = Entity.Unknown>(options?: {
64
64
  subspaceTag?: QueueSubspaceTag;
65
65
  }): Effect.Effect<Queue<T>, never, QueueService> =>
66
66
  QueueService.pipe(Effect.map(({ queues }) => queues.create<T>(options)));
67
67
 
68
- static append = <T extends Obj.Any | Relation.Any = Obj.Any | Relation.Any>(
69
- queue: Queue<T>,
70
- objects: T[],
71
- ): Effect.Effect<void> => Effect.promise(() => queue.append(objects));
68
+ static append = <T extends Entity.Unknown = Entity.Unknown>(queue: Queue<T>, objects: T[]): Effect.Effect<void> =>
69
+ Effect.promise(() => queue.append(objects));
72
70
  }
73
71
 
74
72
  /**
@@ -8,7 +8,7 @@ import * as Layer from 'effect/Layer';
8
8
 
9
9
  import { AgentStatus } from '@dxos/ai';
10
10
  import { Obj } from '@dxos/echo';
11
- import type { ObjectId } from '@dxos/echo/internal';
11
+ import { type ObjectId } from '@dxos/keys';
12
12
  import { Message } from '@dxos/types';
13
13
 
14
14
  /**
@@ -4,8 +4,8 @@
4
4
 
5
5
  import * as Schema from 'effect/Schema';
6
6
 
7
- import { Obj, Type } from '@dxos/echo';
8
- import { JsonSchemaType, LabelAnnotation, Ref } from '@dxos/echo/internal';
7
+ import { Annotation, JsonSchema, Obj, Type } from '@dxos/echo';
8
+ import { SystemTypeAnnotation } from '@dxos/echo/internal';
9
9
 
10
10
  import { Script } from './Script';
11
11
 
@@ -24,7 +24,6 @@ export const Function = Schema.Struct({
24
24
  description: 'Unique registration key for the blueprint',
25
25
  }),
26
26
 
27
- // TODO(burdon): Rename to id/uri?
28
27
  name: Schema.NonEmptyString,
29
28
  version: Schema.String,
30
29
 
@@ -37,10 +36,10 @@ export const Function = Schema.Struct({
37
36
 
38
37
  // Reference to a source script if it exists within ECHO.
39
38
  // TODO(burdon): Don't ref ScriptType directly (core).
40
- source: Schema.optional(Ref(Script)),
39
+ source: Schema.optional(Type.Ref(Script)),
41
40
 
42
- inputSchema: Schema.optional(JsonSchemaType),
43
- outputSchema: Schema.optional(JsonSchemaType),
41
+ inputSchema: Schema.optional(JsonSchema.JsonSchema),
42
+ outputSchema: Schema.optional(JsonSchema.JsonSchema),
44
43
 
45
44
  /**
46
45
  * List of required services.
@@ -55,8 +54,27 @@ export const Function = Schema.Struct({
55
54
  typename: 'dxos.org/type/Function',
56
55
  version: '0.1.0',
57
56
  }),
58
- LabelAnnotation.set(['name']),
57
+ Annotation.LabelAnnotation.set(['name']),
58
+ SystemTypeAnnotation.set(true),
59
59
  );
60
+
60
61
  export interface Function extends Schema.Schema.Type<typeof Function> {}
61
62
 
62
63
  export const make = (props: Obj.MakeProps<typeof Function>) => Obj.make(Function, props);
64
+
65
+ /**
66
+ * Copies properties from source to target.
67
+ * @param target - Target object to copy properties to.
68
+ * @param source - Source object to copy properties from.
69
+ */
70
+ export const setFrom = (target: Function, source: Function) => {
71
+ target.key = source.key ?? target.key;
72
+ target.name = source.name ?? target.name;
73
+ target.version = source.version;
74
+ target.description = source.description;
75
+ target.updated = source.updated;
76
+ // TODO(dmaretskyi): A workaround for an ECHO bug.
77
+ target.inputSchema = source.inputSchema ? JSON.parse(JSON.stringify(source.inputSchema)) : undefined;
78
+ target.outputSchema = source.outputSchema ? JSON.parse(JSON.stringify(source.outputSchema)) : undefined;
79
+ Obj.getMeta(target).keys = JSON.parse(JSON.stringify(Obj.getMeta(source).keys));
80
+ };
@@ -4,8 +4,8 @@
4
4
 
5
5
  import * as Schema from 'effect/Schema';
6
6
 
7
- import { Obj, Ref, Type } from '@dxos/echo';
8
- import { FormAnnotation, LabelAnnotation } from '@dxos/echo/internal';
7
+ import { Annotation, Obj, Ref, Type } from '@dxos/echo';
8
+ import { FormInputAnnotation } from '@dxos/echo/internal';
9
9
  import { Text } from '@dxos/schema';
10
10
 
11
11
  /**
@@ -16,14 +16,14 @@ export const Script = Schema.Struct({
16
16
  description: Schema.String.pipe(Schema.optional),
17
17
  // TODO(burdon): Change to hash of deployed content.
18
18
  // Whether source has changed since last deploy.
19
- changed: Schema.Boolean.pipe(FormAnnotation.set(false), Schema.optional),
20
- source: Type.Ref(Text.Text).pipe(FormAnnotation.set(false)),
19
+ changed: Schema.Boolean.pipe(FormInputAnnotation.set(false), Schema.optional),
20
+ source: Type.Ref(Text.Text).pipe(FormInputAnnotation.set(false)),
21
21
  }).pipe(
22
22
  Type.Obj({
23
23
  typename: 'dxos.org/type/Script',
24
24
  version: '0.1.0',
25
25
  }),
26
- LabelAnnotation.set(['name']),
26
+ Annotation.LabelAnnotation.set(['name']),
27
27
  );
28
28
  export interface Script extends Schema.Schema.Type<typeof Script> {}
29
29
 
@@ -6,7 +6,7 @@ import * as Schema from 'effect/Schema';
6
6
  import * as SchemaAST from 'effect/SchemaAST';
7
7
 
8
8
  import { Obj, QueryAST, Type } from '@dxos/echo';
9
- import { Expando, OptionsAnnotationId, Ref } from '@dxos/echo/internal';
9
+ import { OptionsAnnotationId, SystemTypeAnnotation } from '@dxos/echo/internal';
10
10
  import { DXN } from '@dxos/keys';
11
11
 
12
12
  /**
@@ -101,7 +101,7 @@ const Trigger_ = Schema.Struct({
101
101
  * Function or workflow to invoke.
102
102
  */
103
103
  // TODO(dmaretskyi): Can be a Ref(FunctionType) or Ref(ComputeGraphType).
104
- function: Schema.optional(Ref(Expando).annotations({ title: 'Function' })),
104
+ function: Schema.optional(Type.Ref(Type.Expando).annotations({ title: 'Function' })),
105
105
 
106
106
  /**
107
107
  * Only used for workflowSchema.
@@ -131,6 +131,7 @@ const Trigger_ = Schema.Struct({
131
131
  typename: 'dxos.org/type/Trigger',
132
132
  version: '0.1.0',
133
133
  }),
134
+ SystemTypeAnnotation.set(true),
134
135
  );
135
136
  export interface Trigger extends Schema.Schema.Type<typeof Trigger_> {}
136
137
  export interface TriggerEncoded extends Schema.Schema.Encoded<typeof Trigger_> {}