Skip to content

Commit

Permalink
Add onReady method to particles. (#4808)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
SHeimlich committed Mar 4, 2020
1 parent 86b011c commit ddcf336
Show file tree
Hide file tree
Showing 4 changed files with 211 additions and 17 deletions.
6 changes: 3 additions & 3 deletions src/runtime/particle-execution-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,11 +284,11 @@ export class ParticleExecutionContext implements StorageCommunicationEndpointPro
});

return [particle, async () => {
if (reinstantiate) {
particle.setCreated();
}
await this.assignHandle(particle, spec, id, handleMap, p);
resolve();
if (!reinstantiate) {
particle.onCreate();
}
}];
}

Expand Down
31 changes: 25 additions & 6 deletions src/runtime/particle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export class Particle {
private _idle: Promise<void> = Promise.resolve();
private _idleResolver: Runnable;
private _busy = 0;
private created: boolean;

protected _handlesToSync: number;

Expand All @@ -50,18 +51,36 @@ export class Particle {
if (this.spec.inputs.length === 0) {
this.extraData = true;
}
this.created = false;
}

callOnCreate(): void {
if (this.created) return;
this.created = true;
this.onCreate();
}

/**
* Called after handles are synced, override to provide initial processing.
* Called after handles are writable, only on first initialization of particle.
*/
protected ready(): void {
protected onCreate(): void {}

callOnReady(): void {
if (!this.created) {
this.callOnCreate();
}
this.onReady();
}

setCreated(): void {
this.created = true;
}

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

/**
* This sets the capabilities for this particle. This can only
Expand Down Expand Up @@ -94,7 +113,7 @@ export class Particle {
this.onError = onException;
if (!this._handlesToSync) {
// onHandleSync is called IFF there are input handles, otherwise we are ready now
this.ready();
this.callOnReady();
}
}

Expand Down Expand Up @@ -123,7 +142,7 @@ export class Particle {
await this.invokeSafely(async p => p.onHandleSync(handle, model), onException);
// once we've synced each readable handle, we are ready to start
if (--this._handlesToSync === 0) {
this.ready();
this.callOnReady();
}
}

Expand Down
188 changes: 181 additions & 7 deletions src/runtime/tests/particle-interface-loading-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ import {handleNGFor, SingletonHandle} from '../storageNG/handle.js';
import {Entity} from '../entity.js';
import {singletonHandle, SingletonInterfaceStore, SingletonEntityStore} from '../storageNG/storage-ng.js';

async function mapHandleToStore(arc, recipe, classType, id) {
const store = await arc.createStore(new SingletonType(classType.type), undefined, `test:${id}`);
const storageProxy = new StorageProxy('id', await store.activate(), new SingletonType(classType.type), store.storageKey.toString());
const handle = await handleNGFor('crdt-key', storageProxy, arc.idGenerator, null, true, true, classType.toString()) as SingletonHandle<Entity>;
recipe.handles[id].mapToStorage(store);
return handle;
}

describe('particle interface loading', () => {

it('loads interfaces into particles', async () => {
Expand Down Expand Up @@ -253,9 +261,9 @@ describe('particle interface loading', () => {
defineParticle(({Particle}) => {
var created = false;
return class extends Particle {
async onCreate() {
onCreate() {
this.innerFooHandle = this.handles.get('innerFoo');
await this.innerFooHandle.set(new this.innerFooHandle.entityClass({value: "Created!"}));
this.innerFooHandle.set(new this.innerFooHandle.entityClass({value: "Created!"}));
created = true;
}
async onHandleSync(handle, model) {
Expand All @@ -272,11 +280,7 @@ describe('particle interface loading', () => {
const storageKey = new VolatileStorageKey(id, 'unique');
const arc = new Arc({id, storageKey, loader, context: manifest});
const fooClass = Entity.createEntityClass(manifest.findSchemaByName('Foo'), null);

const fooStore = await arc.createStore(new SingletonType(fooClass.type), undefined, 'test:0');
const varStorageProxy = new StorageProxy('id', await fooStore.activate(), new SingletonType(fooClass.type), fooStore.storageKey.toString());
const fooHandle = await handleNGFor('crdt-key', varStorageProxy, arc.idGenerator, null, true, true, 'fooHandle') as SingletonHandle<Entity>;
recipe.handles[0].mapToStorage(fooStore);
const fooHandle = await mapHandleToStore(arc, recipe, fooClass, 0);

recipe.normalize();
await arc.instantiate(recipe);
Expand All @@ -293,4 +297,174 @@ describe('particle interface loading', () => {
const fooHandle2 = await handleNGFor('crdt-key', varStorageProxy2, arc2.idGenerator, null, true, true, 'varHandle') as SingletonHandle<Entity>;
assert.deepStrictEqual(await fooHandle2.fetch(), new fooClass({value: 'Not created!'}));
});

it('onReady sees overriden values in onCreate', async () => {
const manifest = await Manifest.parse(`
schema Foo
value: Text
particle UpdatingParticle in 'updating-particle.js'
bar: reads writes Foo
recipe
h1: use *
UpdatingParticle
bar: h1
`);
assert.lengthOf(manifest.recipes, 1);
const recipe = manifest.recipes[0];
const loader = new Loader(null, {
'updating-particle.js': `
'use strict';
defineParticle(({Particle}) => {
var handlesSynced = 0;
return class extends Particle {
onCreate() {
this.barHandle = this.handles.get('bar');
this.barHandle.set(new this.barHandle.entityClass({value: "Created!"}));
}
async onReady() {
this.barHandle = this.handles.get('bar');
this.bar = await this.barHandle.fetch();
if(this.bar.value == "Created!") {
await this.barHandle.set(new this.barHandle.entityClass({value: "Ready!"}))
} else {
await this.barHandle.set(new this.barHandle.entityClass({value: "Handle not overriden by onCreate!"}))
}
}
};
});
`
});
const id = ArcId.newForTest('test');
const storageKey = new VolatileStorageKey(id, 'unique');
const arc = new Arc({id, storageKey, loader, context: manifest});
const fooClass = Entity.createEntityClass(manifest.findSchemaByName('Foo'), null);

const barHandle = await mapHandleToStore(arc, recipe, fooClass, 0);
await barHandle.set(new fooClass({value: 'Set!'}));

recipe.normalize();
await arc.instantiate(recipe);
await arc.idle;
assert.deepStrictEqual(await barHandle.fetch(), new fooClass({value: 'Ready!'}));
});

it('onReady runs when handles are first synced', async () => {
const manifest = await Manifest.parse(`
schema Foo
value: Text
particle UpdatingParticle in 'updating-particle.js'
innerFoo: reads writes Foo
bar: reads Foo
recipe
h0: use *
h1: use *
UpdatingParticle
innerFoo: h0
bar: h1
`);
assert.lengthOf(manifest.recipes, 1);
const recipe = manifest.recipes[0];
const loader = new Loader(null, {
'updating-particle.js': `
'use strict';
defineParticle(({Particle}) => {
var handlesSynced = 0;
return class extends Particle {
onCreate() {
this.innerFooHandle = this.handles.get('innerFoo');
this.innerFooHandle.set(new this.innerFooHandle.entityClass({value: "Created!"}));
}
onHandleSync(handle, model) {
handlesSynced += 1;
}
async onReady() {
this.innerFooHandle = this.handles.get('innerFoo');
this.foo = await this.innerFooHandle.fetch()
this.barHandle = this.handles.get('bar');
this.bar = await this.barHandle.fetch();
var s = "Ready!";
if(this.foo.value != "Created!") {
s = s + " onCreate was not called before onReady.";
}
if (this.bar.value != "Set!") {
s = s + " Read only handles not initialised in onReady";
}
if (handlesSynced != 2) {
s = s + " Not all handles were synced before onReady was called.";
}
this.innerFooHandle.set(new this.innerFooHandle.entityClass({value: s}))
}
};
});
`
});
const id = ArcId.newForTest('test');
const storageKey = new VolatileStorageKey(id, 'unique');
const arc = new Arc({id, storageKey, loader, context: manifest});
const fooClass = Entity.createEntityClass(manifest.findSchemaByName('Foo'), null);

const fooHandle = await mapHandleToStore(arc, recipe, fooClass, 0);
const barHandle = await mapHandleToStore(arc, recipe, fooClass, 1);

await barHandle.set(new fooClass({value: 'Set!'}));

recipe.normalize();
await arc.instantiate(recipe);
await arc.idle;
assert.deepStrictEqual(await fooHandle.fetch(), new fooClass({value: 'Ready!'}));
});

it('onReady runs when there are no handles to sync', async () => {
const manifest = await Manifest.parse(`
schema Foo
value: Text
particle UpdatingParticle in 'updating-particle.js'
innerFoo: writes Foo
recipe
h0: use *
UpdatingParticle
innerFoo: h0
`);
assert.lengthOf(manifest.recipes, 1);
const recipe = manifest.recipes[0];
const loader = new Loader(null, {
'updating-particle.js': `
'use strict';
defineParticle(({Particle}) => {
var created = false;
return class extends Particle {
onCreate() {
created = true;
}
onReady(handle, model) {
this.innerFooHandle = this.handles.get('innerFoo');
if (created) {
this.innerFooHandle.set(new this.innerFooHandle.entityClass({value: "Created!"}));
} else {
this.innerFooHandle.set(new this.innerFooHandle.entityClass({value: "Not created!"}));
}
}
};
});
`
});
const id = ArcId.newForTest('test');
const storageKey = new VolatileStorageKey(id, 'unique');
const arc = new Arc({id, storageKey, loader, context: manifest});
const fooClass = Entity.createEntityClass(manifest.findSchemaByName('Foo'), null);

const fooHandle = await mapHandleToStore(arc, recipe, fooClass, 0);

recipe.normalize();
await arc.instantiate(recipe);
await arc.idle;
assert.deepStrictEqual(await fooHandle.fetch(), new fooClass({value: 'Created!'}));
});
});
3 changes: 2 additions & 1 deletion src/runtime/ui-particle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,10 @@ export class UiParticle extends XenStateMixin(UiParticleBase) {
return setTimeout(done, 10);
}

ready() {
onReady() : void {
// ensure we `update()` at least once
this._invalidate();
super.onReady();
}

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

0 comments on commit ddcf336

Please sign in to comment.