@dxos/functions 0.3.9-main.c2ac8a5 → 0.3.9-main.cdfefb1
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/dist/lib/browser/index.mjs +115 -72
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +118 -76
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/types/src/handler.d.ts +9 -2
- package/dist/types/src/handler.d.ts.map +1 -1
- package/dist/types/src/manifest.d.ts +1 -1
- package/dist/types/src/manifest.d.ts.map +1 -1
- package/dist/types/src/runtime/scheduler.d.ts +8 -3
- package/dist/types/src/runtime/scheduler.d.ts.map +1 -1
- package/dist/types/src/runtime/scheduler.test.d.ts +2 -0
- package/dist/types/src/runtime/scheduler.test.d.ts.map +1 -0
- package/package.json +9 -9
- package/src/handler.ts +28 -3
- package/src/manifest.ts +2 -2
- package/src/runtime/scheduler.test.ts +57 -0
- package/src/runtime/scheduler.ts +95 -72
- package/dist/types/src/function.test.d.ts +0 -2
- package/dist/types/src/function.test.d.ts.map +0 -1
- package/src/function.test.ts +0 -7
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { expect } from 'chai';
|
|
6
|
+
|
|
7
|
+
import { Trigger } from '@dxos/async';
|
|
8
|
+
import { Client } from '@dxos/client';
|
|
9
|
+
import { TestBuilder } from '@dxos/client/testing';
|
|
10
|
+
import { describe, test } from '@dxos/test';
|
|
11
|
+
|
|
12
|
+
import { Scheduler } from './scheduler';
|
|
13
|
+
import { type FunctionManifest } from '../manifest';
|
|
14
|
+
|
|
15
|
+
describe('scheduler', () => {
|
|
16
|
+
test('basic', async () => {
|
|
17
|
+
const testBuilder = new TestBuilder();
|
|
18
|
+
const client = new Client({ services: testBuilder.createLocal() });
|
|
19
|
+
await client.initialize();
|
|
20
|
+
await client.halo.createIdentity();
|
|
21
|
+
|
|
22
|
+
const manifest: FunctionManifest = {
|
|
23
|
+
functions: [
|
|
24
|
+
{
|
|
25
|
+
id: 'example.com/function/test',
|
|
26
|
+
name: 'test',
|
|
27
|
+
handler: 'test',
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
triggers: [
|
|
31
|
+
{
|
|
32
|
+
function: 'example.com/function/test',
|
|
33
|
+
schedule: '* * * * * *', // Every second.
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
let count = 0;
|
|
39
|
+
const done = new Trigger();
|
|
40
|
+
const scheduler = new Scheduler(client, manifest, {
|
|
41
|
+
callback: async () => {
|
|
42
|
+
if (++count === 3) {
|
|
43
|
+
done.wake();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return 200;
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
await scheduler.start();
|
|
51
|
+
await done.wait();
|
|
52
|
+
expect(count).to.equal(3);
|
|
53
|
+
|
|
54
|
+
await scheduler.stop();
|
|
55
|
+
await client.destroy();
|
|
56
|
+
});
|
|
57
|
+
});
|
package/src/runtime/scheduler.ts
CHANGED
|
@@ -13,10 +13,14 @@ import { invariant } from '@dxos/invariant';
|
|
|
13
13
|
import { log } from '@dxos/log';
|
|
14
14
|
import { ComplexMap } from '@dxos/util';
|
|
15
15
|
|
|
16
|
-
import { type
|
|
16
|
+
import { type FunctionSubscriptionEvent } from '../handler';
|
|
17
|
+
import { type FunctionDef, type FunctionManifest, type FunctionTrigger, type TriggerSubscription } from '../manifest';
|
|
18
|
+
|
|
19
|
+
type Callback = (data: FunctionSubscriptionEvent) => Promise<number>;
|
|
17
20
|
|
|
18
21
|
type SchedulerOptions = {
|
|
19
|
-
endpoint
|
|
22
|
+
endpoint?: string;
|
|
23
|
+
callback?: Callback;
|
|
20
24
|
};
|
|
21
25
|
|
|
22
26
|
/**
|
|
@@ -33,7 +37,7 @@ export class Scheduler {
|
|
|
33
37
|
constructor(
|
|
34
38
|
private readonly _client: Client,
|
|
35
39
|
private readonly _manifest: FunctionManifest,
|
|
36
|
-
private readonly _options: SchedulerOptions,
|
|
40
|
+
private readonly _options: SchedulerOptions = {},
|
|
37
41
|
) {}
|
|
38
42
|
|
|
39
43
|
async start() {
|
|
@@ -58,6 +62,7 @@ export class Scheduler {
|
|
|
58
62
|
const def = this._manifest.functions.find((config) => config.id === trigger.function);
|
|
59
63
|
invariant(def, `Function not found: ${trigger.function}`);
|
|
60
64
|
|
|
65
|
+
// Currently supports only one trigger declaration per function.
|
|
61
66
|
const exists = this._mounts.get(key);
|
|
62
67
|
if (!exists) {
|
|
63
68
|
this._mounts.set(key, { ctx, trigger });
|
|
@@ -66,69 +71,14 @@ export class Scheduler {
|
|
|
66
71
|
return;
|
|
67
72
|
}
|
|
68
73
|
|
|
69
|
-
//
|
|
74
|
+
// Timer.
|
|
70
75
|
if (trigger.schedule) {
|
|
71
|
-
|
|
72
|
-
await this.execFunction(def, {
|
|
73
|
-
space: space.key,
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
// TODO(burdon): Check greater than 30s min (use cron-parser).
|
|
78
|
-
const job = new CronJob(trigger.schedule, () => task.schedule());
|
|
79
|
-
|
|
80
|
-
job.start();
|
|
81
|
-
ctx.onDispose(() => job.stop());
|
|
76
|
+
this._createTimer(ctx, space, def, trigger);
|
|
82
77
|
}
|
|
83
78
|
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const task = new DeferredTask(ctx, async () => {
|
|
88
|
-
await this.execFunction(def, {
|
|
89
|
-
space: space.key,
|
|
90
|
-
objects: Array.from(objectIds),
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
// TODO(burdon): Standardize subscription handles.
|
|
95
|
-
const subscriptions: (() => void)[] = [];
|
|
96
|
-
const subscription = createSubscription(({ added, updated }) => {
|
|
97
|
-
for (const object of added) {
|
|
98
|
-
objectIds.add(object.id);
|
|
99
|
-
}
|
|
100
|
-
for (const object of updated) {
|
|
101
|
-
objectIds.add(object.id);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
task.schedule();
|
|
105
|
-
});
|
|
106
|
-
subscriptions.push(() => subscription.unsubscribe());
|
|
107
|
-
|
|
108
|
-
const { type, props, deep, delay } = trigger.subscription;
|
|
109
|
-
const update = ({ objects }: Query) => {
|
|
110
|
-
subscription.update(objects);
|
|
111
|
-
|
|
112
|
-
// TODO(burdon): Hack to monitor changes to Document's text object.
|
|
113
|
-
if (deep) {
|
|
114
|
-
log.info('update', { type, deep, objects: objects.length });
|
|
115
|
-
for (const object of objects) {
|
|
116
|
-
const content = object.content;
|
|
117
|
-
if (content instanceof TextObject) {
|
|
118
|
-
subscriptions.push(content[subscribe](debounce(() => subscription.update([object]), 1_000)));
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
// TODO(burdon): [Bug]: all callbacks are fired on the first mutation.
|
|
125
|
-
// TODO(burdon): [Bug]: not updated when document is deleted (either top or hierarchically).
|
|
126
|
-
const query = space.db.query(Filter.typename(type, props));
|
|
127
|
-
subscriptions.push(query.subscribe(delay ? debounce(update, delay * 1_000) : update));
|
|
128
|
-
|
|
129
|
-
ctx.onDispose(() => {
|
|
130
|
-
subscriptions.forEach((unsubscribe) => unsubscribe());
|
|
131
|
-
});
|
|
79
|
+
// Subscription.
|
|
80
|
+
for (const triggerSubscription of trigger.subscriptions ?? []) {
|
|
81
|
+
this._createSubscription(ctx, space, def, triggerSubscription);
|
|
132
82
|
}
|
|
133
83
|
}
|
|
134
84
|
}
|
|
@@ -142,19 +92,92 @@ export class Scheduler {
|
|
|
142
92
|
}
|
|
143
93
|
}
|
|
144
94
|
|
|
145
|
-
private
|
|
95
|
+
private _createTimer(ctx: Context, space: Space, def: FunctionDef, trigger: FunctionTrigger) {
|
|
96
|
+
const task = new DeferredTask(ctx, async () => {
|
|
97
|
+
await this._execFunction(def, {
|
|
98
|
+
space: space.key,
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// TODO(burdon): Check greater than 30s min (use cron-parser).
|
|
103
|
+
invariant(trigger.schedule);
|
|
104
|
+
const job = new CronJob(trigger.schedule, () => task.schedule());
|
|
105
|
+
|
|
106
|
+
job.start();
|
|
107
|
+
ctx.onDispose(() => job.stop());
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private _createSubscription(ctx: Context, space: Space, def: FunctionDef, triggerSubscription: TriggerSubscription) {
|
|
111
|
+
const objectIds = new Set<string>();
|
|
112
|
+
const task = new DeferredTask(ctx, async () => {
|
|
113
|
+
await this._execFunction(def, {
|
|
114
|
+
space: space.key,
|
|
115
|
+
objects: Array.from(objectIds),
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// TODO(burdon): Standardize subscription handles.
|
|
120
|
+
const subscriptions: (() => void)[] = [];
|
|
121
|
+
const subscription = createSubscription(({ added, updated }) => {
|
|
122
|
+
for (const object of added) {
|
|
123
|
+
objectIds.add(object.id);
|
|
124
|
+
}
|
|
125
|
+
for (const object of updated) {
|
|
126
|
+
objectIds.add(object.id);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
task.schedule();
|
|
130
|
+
});
|
|
131
|
+
subscriptions.push(() => subscription.unsubscribe());
|
|
132
|
+
|
|
133
|
+
const { type, props, deep, delay } = triggerSubscription;
|
|
134
|
+
const update = ({ objects }: Query) => {
|
|
135
|
+
subscription.update(objects);
|
|
136
|
+
|
|
137
|
+
// TODO(burdon): Hack to monitor changes to Document's text object.
|
|
138
|
+
if (deep) {
|
|
139
|
+
log.info('update', { type, deep, objects: objects.length });
|
|
140
|
+
for (const object of objects) {
|
|
141
|
+
const content = object.content;
|
|
142
|
+
if (content instanceof TextObject) {
|
|
143
|
+
subscriptions.push(content[subscribe](debounce(() => subscription.update([object]), 1_000)));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// TODO(burdon): [Bug]: all callbacks are fired on the first mutation.
|
|
150
|
+
// TODO(burdon): [Bug]: not updated when document is deleted (either top or hierarchically).
|
|
151
|
+
const query = space.db.query(Filter.typename(type, props));
|
|
152
|
+
subscriptions.push(query.subscribe(delay ? debounce(update, delay * 1_000) : update));
|
|
153
|
+
|
|
154
|
+
ctx.onDispose(() => {
|
|
155
|
+
subscriptions.forEach((unsubscribe) => unsubscribe());
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private async _execFunction(def: FunctionDef, data: any) {
|
|
146
160
|
try {
|
|
147
161
|
log('request', { function: def.id });
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
162
|
+
const { endpoint, callback } = this._options;
|
|
163
|
+
let status = 0;
|
|
164
|
+
if (endpoint) {
|
|
165
|
+
// TODO(burdon): Move out of scheduler (generalize as callback).
|
|
166
|
+
const response = await fetch(`${this._options.endpoint}/${def.name}`, {
|
|
167
|
+
method: 'POST',
|
|
168
|
+
headers: {
|
|
169
|
+
'Content-Type': 'application/json',
|
|
170
|
+
},
|
|
171
|
+
body: JSON.stringify(data),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
status = response.status;
|
|
175
|
+
} else if (callback) {
|
|
176
|
+
status = await callback(data);
|
|
177
|
+
}
|
|
155
178
|
|
|
156
179
|
// const result = await response.json();
|
|
157
|
-
log('result', { function: def.id, result:
|
|
180
|
+
log('result', { function: def.id, result: status });
|
|
158
181
|
} catch (err: any) {
|
|
159
182
|
log.error('error', { function: def.id, error: err.message });
|
|
160
183
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"function.test.d.ts","sourceRoot":"","sources":["../../../src/function.test.ts"],"names":[],"mappings":""}
|