From 041e259c11e065f8451052a300dd2e0c8b9fd59f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 8 Feb 2021 03:03:28 +0100 Subject: [PATCH] OSC impl, OscParser tests --- src/common/parser/EscapeSequenceParser.ts | 20 ++- src/common/parser/OscParser.test.ts | 201 ++++++++++++++++++++++ src/common/parser/OscParser.ts | 86 ++++++--- src/common/parser/Types.d.ts | 4 +- 4 files changed, 277 insertions(+), 34 deletions(-) diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index 842a99b8..d2da17c6 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -548,6 +548,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } } } + this._parseStack.handlers = []; break; case ParserStackType.ESC: if (promiseResult === false && handlerPos > -1) { @@ -562,10 +563,11 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } } } + this._parseStack.handlers = []; break; case ParserStackType.DCS: code = data[this._parseStack.chunkPos]; - if (handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a)) { + if (handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a, promiseResult)) { return handlerResult; } if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE; @@ -574,7 +576,14 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._collect = 0; break; case ParserStackType.OSC: - // TODO + code = data[this._parseStack.chunkPos]; + if (handlerResult = this._oscParser.end(code !== 0x18 && code !== 0x1a, promiseResult)) { + return handlerResult; + } + if (code === 0x1b) transition |= ParserState.ESCAPE; + this._params.reset(); + this._params.addParam(0); // ZDM + this._collect = 0; break; } // cleanup before continuing with the main loop @@ -717,7 +726,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP break; case ParserAction.DCS_UNHOOK: if (handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a)) { - this._parseStack.state = ParserStackType.DCS; + this._preserveStack(ParserStackType.DCS, [], 0, transition, i); return handlerResult; } if (code === 0x1b) transition |= ParserState.ESCAPE; @@ -740,7 +749,10 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } break; case ParserAction.OSC_END: - this._oscParser.end(code !== 0x18 && code !== 0x1a); + if (handlerResult = this._oscParser.end(code !== 0x18 && code !== 0x1a)) { + this._preserveStack(ParserStackType.OSC, [], 0, transition, i); + return handlerResult; + } if (code === 0x1b) transition |= ParserState.ESCAPE; this._params.reset(); this._params.addParam(0); // ZDM diff --git a/src/common/parser/OscParser.test.ts b/src/common/parser/OscParser.test.ts index e2c8641c..5c7f5777 100644 --- a/src/common/parser/OscParser.test.ts +++ b/src/common/parser/OscParser.test.ts @@ -250,3 +250,204 @@ describe('OscParser', () => { }); }); }); + + +class TestHandlerAsync implements IOscHandler { + constructor(public id: number, public output: any[], public msg: string, public returnFalse: boolean = false) {} + public start(): void { + this.output.push([this.msg, this.id, 'START']); + } + public put(data: Uint32Array, start: number, end: number): void { + this.output.push([this.msg, this.id, 'PUT', utf32ToString(data, start, end)]); + } + public async end(success: boolean): Promise { + await new Promise(res => setTimeout(res, 20)); + this.output.push([this.msg, this.id, 'END', success]); + if (this.returnFalse) { + return false; + } + return true; + } +} +async function endP(parser: OscParser, success: boolean): Promise { + let result: void | Promise; + let prev: boolean | undefined; + while (result = parser.end(success, prev)) { + prev = await result; + } +} + +describe('OscParser - async tests', () => { + let parser: OscParser; + let reports: any[] = []; + beforeEach(() => { + reports = []; + parser = new OscParser(); + parser.setHandlerFallback((id, action, data) => { + reports.push([id, action, data]); + }); + }); + describe('sync and async mixed', () => { + describe('sync | async | sync', () => { + it('first should run, cleanup action for others', async () => { + parser.registerHandler(1234, new TestHandler(1234, reports, 's1')); + parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 'a1')); + parser.registerHandler(1234, new TestHandler(1234, reports, 's2')); + parser.start(); + let data = toUtf32('1234;Here comes'); + parser.put(data, 0, data.length); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + await endP(parser, true); + assert.deepEqual(reports, [ + // messages from TestHandler + ['s2', 1234, 'START'], + ['a1', 1234, 'START'], + ['s1', 1234, 'START'], + ['s2', 1234, 'PUT', 'Here comes'], + ['a1', 1234, 'PUT', 'Here comes'], + ['s1', 1234, 'PUT', 'Here comes'], + ['s2', 1234, 'PUT', 'the mouse!'], + ['a1', 1234, 'PUT', 'the mouse!'], + ['s1', 1234, 'PUT', 'the mouse!'], + ['s2', 1234, 'END', true], + ['a1', 1234, 'END', false], + ['s1', 1234, 'END', false] + ]); + }); + it('all should run', async () => { + parser.registerHandler(1234, new TestHandler(1234, reports, 's1', true)); + parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 'a1', true)); + parser.registerHandler(1234, new TestHandler(1234, reports, 's2', true)); + parser.start(); + let data = toUtf32('1234;Here comes'); + parser.put(data, 0, data.length); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + await endP(parser, true); + assert.deepEqual(reports, [ + // messages from TestHandler + ['s2', 1234, 'START'], + ['a1', 1234, 'START'], + ['s1', 1234, 'START'], + ['s2', 1234, 'PUT', 'Here comes'], + ['a1', 1234, 'PUT', 'Here comes'], + ['s1', 1234, 'PUT', 'Here comes'], + ['s2', 1234, 'PUT', 'the mouse!'], + ['a1', 1234, 'PUT', 'the mouse!'], + ['s1', 1234, 'PUT', 'the mouse!'], + ['s2', 1234, 'END', true], + ['a1', 1234, 'END', true], + ['s1', 1234, 'END', true] + ]); + }); + }); + describe('async | sync | async', () => { + it('first should run, cleanup action for others', async () => { + parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 's1')); + parser.registerHandler(1234, new TestHandler(1234, reports, 'a1')); + parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 's2')); + parser.start(); + let data = toUtf32('1234;Here comes'); + parser.put(data, 0, data.length); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + await endP(parser, true); + assert.deepEqual(reports, [ + // messages from TestHandler + ['s2', 1234, 'START'], + ['a1', 1234, 'START'], + ['s1', 1234, 'START'], + ['s2', 1234, 'PUT', 'Here comes'], + ['a1', 1234, 'PUT', 'Here comes'], + ['s1', 1234, 'PUT', 'Here comes'], + ['s2', 1234, 'PUT', 'the mouse!'], + ['a1', 1234, 'PUT', 'the mouse!'], + ['s1', 1234, 'PUT', 'the mouse!'], + ['s2', 1234, 'END', true], + ['a1', 1234, 'END', false], + ['s1', 1234, 'END', false] + ]); + }); + it('all should run', async () => { + parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 's1', true)); + parser.registerHandler(1234, new TestHandler(1234, reports, 'a1', true)); + parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 's2', true)); + parser.start(); + let data = toUtf32('1234;Here comes'); + parser.put(data, 0, data.length); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + await endP(parser, true); + assert.deepEqual(reports, [ + // messages from TestHandler + ['s2', 1234, 'START'], + ['a1', 1234, 'START'], + ['s1', 1234, 'START'], + ['s2', 1234, 'PUT', 'Here comes'], + ['a1', 1234, 'PUT', 'Here comes'], + ['s1', 1234, 'PUT', 'Here comes'], + ['s2', 1234, 'PUT', 'the mouse!'], + ['a1', 1234, 'PUT', 'the mouse!'], + ['s1', 1234, 'PUT', 'the mouse!'], + ['s2', 1234, 'END', true], + ['a1', 1234, 'END', true], + ['s1', 1234, 'END', true] + ]); + }); + }); + describe('OscHandlerFactory', () => { + it('should be called once on end(true)', async () => { + parser.registerHandler(1234, new OscHandler(async data => { reports.push([1234, data]); return true; })); + parser.start(); + let data = toUtf32('1234;Here comes'); + parser.put(data, 0, data.length); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + parser.end(true); + await endP(parser, true); + assert.deepEqual(reports, [[1234, 'Here comes the mouse!']]); + }); + it('should not be called on end(false)', async () => { + parser.registerHandler(1234, new OscHandler(async data => { reports.push([1234, data]); return true; })); + parser.start(); + let data = toUtf32('1234;Here comes'); + parser.put(data, 0, data.length); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + await endP(parser, false); + assert.deepEqual(reports, []); + }); + it('should be disposable', async () => { + parser.registerHandler(1234, new OscHandler(async data => { reports.push(['one', data]); return true; })); + const dispo = parser.registerHandler(1234, new OscHandler(async data => { reports.push(['two', data]); return true; })); + parser.start(); + let data = toUtf32('1234;Here comes'); + parser.put(data, 0, data.length); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + await endP(parser, true); + assert.deepEqual(reports, [['two', 'Here comes the mouse!']]); + dispo.dispose(); + parser.start(); + data = toUtf32('1234;some other'); + parser.put(data, 0, data.length); + data = toUtf32(' data'); + parser.put(data, 0, data.length); + await endP(parser, true); + assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'some other data']]); + }); + it('should respect return false', async () => { + parser.registerHandler(1234, new OscHandler(async data => { reports.push(['one', data]); return true; })); + parser.registerHandler(1234, new OscHandler(async data => { reports.push(['two', data]); return false; })); + parser.start(); + let data = toUtf32('1234;Here comes'); + parser.put(data, 0, data.length); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + await endP(parser, true); + assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'Here comes the mouse!']]); + }); + }); + }); +}); diff --git a/src/common/parser/OscParser.ts b/src/common/parser/OscParser.ts index e7edac66..b0b51e23 100644 --- a/src/common/parser/OscParser.ts +++ b/src/common/parser/OscParser.ts @@ -41,7 +41,7 @@ export class OscParser implements IOscParser { public dispose(): void { this._handlers = Object.create(null); - this._handlerFb = () => {}; + this._handlerFb = () => { }; this._active = EMPTY_HANDLERS; } @@ -76,27 +76,6 @@ export class OscParser implements IOscParser { } } - private _end(success: boolean): void { - // other than the old code we always have to call .end - // to keep the bubbling we use `success` to indicate - // whether a handler should execute - if (!this._active.length) { - this._handlerFb(this._id, 'END', success); - } else { - let j = this._active.length - 1; - for (; j >= 0; j--) { - if (this._active[j].end(success)) { - break; - } - } - j--; - // cleanup left over handlers - for (; j >= 0; j--) { - this._active[j].end(false); - } - } - } - public start(): void { // always reset leftover handlers this.reset(); @@ -137,12 +116,18 @@ export class OscParser implements IOscParser { } } + private _stack = { + paused: false, + loopPosition: 0, + fallThrough: false + }; + /** * Indicates end of an OSC command. * Whether the OSC got aborted or finished normally * is indicated by `success`. */ - public end(success: boolean): void { + public end(success: boolean, promiseResult?: boolean): void | Promise { if (this._state === OscState.START) { return; } @@ -154,7 +139,46 @@ export class OscParser implements IOscParser { if (this._state === OscState.ID) { this._start(); } - this._end(success); + + if (!this._active.length) { + this._handlerFb(this._id, 'END', success); + } else { + let handlerResult: any = false; + let j = this._active.length - 1; + let fallThrough = false; + if (this._stack.paused) { + j = this._stack.loopPosition - 1; + handlerResult = promiseResult; + fallThrough = this._stack.fallThrough; + this._stack.paused = false; + } + if (!fallThrough && handlerResult === false) { + for (; j >= 0; j--) { + if ((handlerResult = this._active[j].end(success)) !== false) { + if (handlerResult instanceof Promise) { + this._stack.paused = true; + this._stack.loopPosition = j; + this._stack.fallThrough = false; + return handlerResult; + } + break; + } + } + j--; + } + // cleanup left over handlers + // we always have to call .end for proper cleanup, + // here we use `success` to indicate whether a handler should execute + for (; j >= 0; j--) { + if ((handlerResult = this._active[j].end(false)) instanceof Promise) { + this._stack.paused = true; + this._stack.loopPosition = j; + this._stack.fallThrough = true; + return handlerResult; + } + } + } + } this._active = EMPTY_HANDLERS; this._id = -1; @@ -170,7 +194,7 @@ export class OscHandler implements IOscHandler { private _data = ''; private _hitLimit: boolean = false; - constructor(private _handler: (data: string) => boolean) {} + constructor(private _handler: (data: string) => boolean | Promise) { } public start(): void { this._data = ''; @@ -188,12 +212,18 @@ export class OscHandler implements IOscHandler { } } - public end(success: boolean): boolean { - let ret = false; + public end(success: boolean): boolean | Promise { + let ret: boolean | Promise = false; if (this._hitLimit) { ret = false; } else if (success) { - ret = this._handler(this._data); + if ((ret = this._handler(this._data)) instanceof Promise) { + return ret.then(res => { + this._data = ''; + this._hitLimit = false; + return res; + }); + } } this._data = ''; this._hitLimit = false; diff --git a/src/common/parser/Types.d.ts b/src/common/parser/Types.d.ts index 30217cc6..dda798cc 100644 --- a/src/common/parser/Types.d.ts +++ b/src/common/parser/Types.d.ts @@ -130,7 +130,7 @@ export interface IOscHandler { * execution of the command should depend on `success`. * To save memory also cleanup data structures here. */ - end(success: boolean): boolean; + end(success: boolean): boolean | Promise; } export type OscFallbackHandlerType = (ident: number, action: 'START' | 'PUT' | 'END', payload?: any) => void; @@ -214,7 +214,7 @@ export interface ISubParser extends IDisposable { export interface IOscParser extends ISubParser { start(): void; - end(success: boolean): void; + end(success: boolean, promiseResult?: boolean): void | Promise; } export interface IDcsParser extends ISubParser {