Skip to content

Commit ddcf336

Browse files
authored
Add onReady method to particles. (#4808)
* Add onReady method to particles. * Rename ready to onReady, update comment * Better particle lifecycle testing. * Ensure onCreate is called before onReady, add more tests on the lifecycle. * Style fixes. * Make onCreate and onReady async, small cleanups on tests. * Lint fixes for promises * Fix up comment. * Fix async
1 parent 86b011c commit ddcf336

File tree

4 files changed

+211
-17
lines changed

4 files changed

+211
-17
lines changed

src/runtime/particle-execution-context.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -284,11 +284,11 @@ export class ParticleExecutionContext implements StorageCommunicationEndpointPro
284284
});
285285

286286
return [particle, async () => {
287+
if (reinstantiate) {
288+
particle.setCreated();
289+
}
287290
await this.assignHandle(particle, spec, id, handleMap, p);
288291
resolve();
289-
if (!reinstantiate) {
290-
particle.onCreate();
291-
}
292292
}];
293293
}
294294

src/runtime/particle.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export class Particle {
3737
private _idle: Promise<void> = Promise.resolve();
3838
private _idleResolver: Runnable;
3939
private _busy = 0;
40+
private created: boolean;
4041

4142
protected _handlesToSync: number;
4243

@@ -50,18 +51,36 @@ export class Particle {
5051
if (this.spec.inputs.length === 0) {
5152
this.extraData = true;
5253
}
54+
this.created = false;
55+
}
56+
57+
callOnCreate(): void {
58+
if (this.created) return;
59+
this.created = true;
60+
this.onCreate();
5361
}
5462

5563
/**
56-
* Called after handles are synced, override to provide initial processing.
64+
* Called after handles are writable, only on first initialization of particle.
5765
*/
58-
protected ready(): void {
66+
protected onCreate(): void {}
67+
68+
callOnReady(): void {
69+
if (!this.created) {
70+
this.callOnCreate();
71+
}
72+
this.onReady();
73+
}
74+
75+
setCreated(): void {
76+
this.created = true;
5977
}
6078

6179
/**
62-
* Called after handles are writable, only on first initialization of particle.
80+
* Called after handles are synced the first time, override to provide initial processing.
81+
* This will be called after onCreate, but will not wait for onCreate to finish.
6382
*/
64-
onCreate(): void {}
83+
protected onReady(): void {}
6584

6685
/**
6786
* This sets the capabilities for this particle. This can only
@@ -94,7 +113,7 @@ export class Particle {
94113
this.onError = onException;
95114
if (!this._handlesToSync) {
96115
// onHandleSync is called IFF there are input handles, otherwise we are ready now
97-
this.ready();
116+
this.callOnReady();
98117
}
99118
}
100119

@@ -123,7 +142,7 @@ export class Particle {
123142
await this.invokeSafely(async p => p.onHandleSync(handle, model), onException);
124143
// once we've synced each readable handle, we are ready to start
125144
if (--this._handlesToSync === 0) {
126-
this.ready();
145+
this.callOnReady();
127146
}
128147
}
129148

src/runtime/tests/particle-interface-loading-test.ts

Lines changed: 181 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ import {handleNGFor, SingletonHandle} from '../storageNG/handle.js';
2323
import {Entity} from '../entity.js';
2424
import {singletonHandle, SingletonInterfaceStore, SingletonEntityStore} from '../storageNG/storage-ng.js';
2525

26+
async function mapHandleToStore(arc, recipe, classType, id) {
27+
const store = await arc.createStore(new SingletonType(classType.type), undefined, `test:${id}`);
28+
const storageProxy = new StorageProxy('id', await store.activate(), new SingletonType(classType.type), store.storageKey.toString());
29+
const handle = await handleNGFor('crdt-key', storageProxy, arc.idGenerator, null, true, true, classType.toString()) as SingletonHandle<Entity>;
30+
recipe.handles[id].mapToStorage(store);
31+
return handle;
32+
}
33+
2634
describe('particle interface loading', () => {
2735

2836
it('loads interfaces into particles', async () => {
@@ -253,9 +261,9 @@ describe('particle interface loading', () => {
253261
defineParticle(({Particle}) => {
254262
var created = false;
255263
return class extends Particle {
256-
async onCreate() {
264+
onCreate() {
257265
this.innerFooHandle = this.handles.get('innerFoo');
258-
await this.innerFooHandle.set(new this.innerFooHandle.entityClass({value: "Created!"}));
266+
this.innerFooHandle.set(new this.innerFooHandle.entityClass({value: "Created!"}));
259267
created = true;
260268
}
261269
async onHandleSync(handle, model) {
@@ -272,11 +280,7 @@ describe('particle interface loading', () => {
272280
const storageKey = new VolatileStorageKey(id, 'unique');
273281
const arc = new Arc({id, storageKey, loader, context: manifest});
274282
const fooClass = Entity.createEntityClass(manifest.findSchemaByName('Foo'), null);
275-
276-
const fooStore = await arc.createStore(new SingletonType(fooClass.type), undefined, 'test:0');
277-
const varStorageProxy = new StorageProxy('id', await fooStore.activate(), new SingletonType(fooClass.type), fooStore.storageKey.toString());
278-
const fooHandle = await handleNGFor('crdt-key', varStorageProxy, arc.idGenerator, null, true, true, 'fooHandle') as SingletonHandle<Entity>;
279-
recipe.handles[0].mapToStorage(fooStore);
283+
const fooHandle = await mapHandleToStore(arc, recipe, fooClass, 0);
280284

281285
recipe.normalize();
282286
await arc.instantiate(recipe);
@@ -293,4 +297,174 @@ describe('particle interface loading', () => {
293297
const fooHandle2 = await handleNGFor('crdt-key', varStorageProxy2, arc2.idGenerator, null, true, true, 'varHandle') as SingletonHandle<Entity>;
294298
assert.deepStrictEqual(await fooHandle2.fetch(), new fooClass({value: 'Not created!'}));
295299
});
300+
301+
it('onReady sees overriden values in onCreate', async () => {
302+
const manifest = await Manifest.parse(`
303+
schema Foo
304+
value: Text
305+
306+
particle UpdatingParticle in 'updating-particle.js'
307+
bar: reads writes Foo
308+
recipe
309+
h1: use *
310+
UpdatingParticle
311+
bar: h1
312+
`);
313+
assert.lengthOf(manifest.recipes, 1);
314+
const recipe = manifest.recipes[0];
315+
const loader = new Loader(null, {
316+
'updating-particle.js': `
317+
'use strict';
318+
defineParticle(({Particle}) => {
319+
var handlesSynced = 0;
320+
return class extends Particle {
321+
onCreate() {
322+
this.barHandle = this.handles.get('bar');
323+
this.barHandle.set(new this.barHandle.entityClass({value: "Created!"}));
324+
}
325+
326+
async onReady() {
327+
this.barHandle = this.handles.get('bar');
328+
this.bar = await this.barHandle.fetch();
329+
330+
if(this.bar.value == "Created!") {
331+
await this.barHandle.set(new this.barHandle.entityClass({value: "Ready!"}))
332+
} else {
333+
await this.barHandle.set(new this.barHandle.entityClass({value: "Handle not overriden by onCreate!"}))
334+
}
335+
}
336+
};
337+
});
338+
`
339+
});
340+
const id = ArcId.newForTest('test');
341+
const storageKey = new VolatileStorageKey(id, 'unique');
342+
const arc = new Arc({id, storageKey, loader, context: manifest});
343+
const fooClass = Entity.createEntityClass(manifest.findSchemaByName('Foo'), null);
344+
345+
const barHandle = await mapHandleToStore(arc, recipe, fooClass, 0);
346+
await barHandle.set(new fooClass({value: 'Set!'}));
347+
348+
recipe.normalize();
349+
await arc.instantiate(recipe);
350+
await arc.idle;
351+
assert.deepStrictEqual(await barHandle.fetch(), new fooClass({value: 'Ready!'}));
352+
});
353+
354+
it('onReady runs when handles are first synced', async () => {
355+
const manifest = await Manifest.parse(`
356+
schema Foo
357+
value: Text
358+
359+
particle UpdatingParticle in 'updating-particle.js'
360+
innerFoo: reads writes Foo
361+
bar: reads Foo
362+
recipe
363+
h0: use *
364+
h1: use *
365+
UpdatingParticle
366+
innerFoo: h0
367+
bar: h1
368+
`);
369+
assert.lengthOf(manifest.recipes, 1);
370+
const recipe = manifest.recipes[0];
371+
const loader = new Loader(null, {
372+
'updating-particle.js': `
373+
'use strict';
374+
defineParticle(({Particle}) => {
375+
var handlesSynced = 0;
376+
return class extends Particle {
377+
onCreate() {
378+
this.innerFooHandle = this.handles.get('innerFoo');
379+
this.innerFooHandle.set(new this.innerFooHandle.entityClass({value: "Created!"}));
380+
}
381+
onHandleSync(handle, model) {
382+
handlesSynced += 1;
383+
}
384+
async onReady() {
385+
this.innerFooHandle = this.handles.get('innerFoo');
386+
this.foo = await this.innerFooHandle.fetch()
387+
388+
this.barHandle = this.handles.get('bar');
389+
this.bar = await this.barHandle.fetch();
390+
391+
var s = "Ready!";
392+
if(this.foo.value != "Created!") {
393+
s = s + " onCreate was not called before onReady.";
394+
}
395+
if (this.bar.value != "Set!") {
396+
s = s + " Read only handles not initialised in onReady";
397+
}
398+
if (handlesSynced != 2) {
399+
s = s + " Not all handles were synced before onReady was called.";
400+
}
401+
402+
this.innerFooHandle.set(new this.innerFooHandle.entityClass({value: s}))
403+
}
404+
};
405+
});
406+
`
407+
});
408+
const id = ArcId.newForTest('test');
409+
const storageKey = new VolatileStorageKey(id, 'unique');
410+
const arc = new Arc({id, storageKey, loader, context: manifest});
411+
const fooClass = Entity.createEntityClass(manifest.findSchemaByName('Foo'), null);
412+
413+
const fooHandle = await mapHandleToStore(arc, recipe, fooClass, 0);
414+
const barHandle = await mapHandleToStore(arc, recipe, fooClass, 1);
415+
416+
await barHandle.set(new fooClass({value: 'Set!'}));
417+
418+
recipe.normalize();
419+
await arc.instantiate(recipe);
420+
await arc.idle;
421+
assert.deepStrictEqual(await fooHandle.fetch(), new fooClass({value: 'Ready!'}));
422+
});
423+
424+
it('onReady runs when there are no handles to sync', async () => {
425+
const manifest = await Manifest.parse(`
426+
schema Foo
427+
value: Text
428+
particle UpdatingParticle in 'updating-particle.js'
429+
innerFoo: writes Foo
430+
recipe
431+
h0: use *
432+
UpdatingParticle
433+
innerFoo: h0
434+
`);
435+
assert.lengthOf(manifest.recipes, 1);
436+
const recipe = manifest.recipes[0];
437+
const loader = new Loader(null, {
438+
'updating-particle.js': `
439+
'use strict';
440+
defineParticle(({Particle}) => {
441+
var created = false;
442+
return class extends Particle {
443+
onCreate() {
444+
created = true;
445+
}
446+
onReady(handle, model) {
447+
this.innerFooHandle = this.handles.get('innerFoo');
448+
if (created) {
449+
this.innerFooHandle.set(new this.innerFooHandle.entityClass({value: "Created!"}));
450+
} else {
451+
this.innerFooHandle.set(new this.innerFooHandle.entityClass({value: "Not created!"}));
452+
}
453+
}
454+
};
455+
});
456+
`
457+
});
458+
const id = ArcId.newForTest('test');
459+
const storageKey = new VolatileStorageKey(id, 'unique');
460+
const arc = new Arc({id, storageKey, loader, context: manifest});
461+
const fooClass = Entity.createEntityClass(manifest.findSchemaByName('Foo'), null);
462+
463+
const fooHandle = await mapHandleToStore(arc, recipe, fooClass, 0);
464+
465+
recipe.normalize();
466+
await arc.instantiate(recipe);
467+
await arc.idle;
468+
assert.deepStrictEqual(await fooHandle.fetch(), new fooClass({value: 'Created!'}));
469+
});
296470
});

src/runtime/ui-particle.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,10 @@ export class UiParticle extends XenStateMixin(UiParticleBase) {
103103
return setTimeout(done, 10);
104104
}
105105

106-
ready() {
106+
onReady() : void {
107107
// ensure we `update()` at least once
108108
this._invalidate();
109+
super.onReady();
109110
}
110111

111112
async onHandleSync(handle: Handle<CRDTTypeRecord>, model): Promise<void> {

0 commit comments

Comments
 (0)