@player-ui/async-node-plugin 1.1.0-next.5 → 1.1.0
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/AsyncNodePlugin.native.js +11 -7
- package/dist/AsyncNodePlugin.native.js.map +1 -1
- package/dist/cjs/index.cjs +12 -3
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/index.legacy-esm.js +12 -3
- package/dist/index.mjs +12 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/index.test.ts +137 -0
- package/src/index.ts +20 -3
|
@@ -684,6 +684,143 @@ describe("view", () => {
|
|
|
684
684
|
expect(view?.actions[1]).toBeUndefined();
|
|
685
685
|
});
|
|
686
686
|
|
|
687
|
+
test("preserves callback-streamed content when the hook resolves with undefined", async () => {
|
|
688
|
+
// Regression: a streaming consumer pushes content through the callback while
|
|
689
|
+
// the hook is still pending, then resolves the hook with nothing. Previously
|
|
690
|
+
// the bail path re-parsed the undefined return (last-writer-wins) and
|
|
691
|
+
// reverted the streamed content back to the async placeholder.
|
|
692
|
+
let deferredResolve: ((value: any) => void) | undefined;
|
|
693
|
+
let updateContent: ((content: any) => void) | undefined;
|
|
694
|
+
|
|
695
|
+
const plugin = new AsyncNodePlugin({
|
|
696
|
+
plugins: [new AsyncNodePluginPlugin()],
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
plugin.hooks.onAsyncNode.tap(
|
|
700
|
+
"test",
|
|
701
|
+
async (_node: Node.Async, update: (content: any) => void) => {
|
|
702
|
+
updateContent = update;
|
|
703
|
+
// Resolve the hook with nothing; content is delivered via the callback.
|
|
704
|
+
return new Promise((resolve) => {
|
|
705
|
+
deferredResolve = resolve;
|
|
706
|
+
});
|
|
707
|
+
},
|
|
708
|
+
);
|
|
709
|
+
|
|
710
|
+
const player = new Player({ plugins: [plugin] });
|
|
711
|
+
|
|
712
|
+
player.start(basicFRFWithActions as any);
|
|
713
|
+
|
|
714
|
+
let view = (player.getState() as InProgressState).controllers.view
|
|
715
|
+
.currentView?.lastUpdate;
|
|
716
|
+
|
|
717
|
+
expect(view).toBeDefined();
|
|
718
|
+
expect(view?.actions[1]).toBeUndefined();
|
|
719
|
+
|
|
720
|
+
// Wait until the hook has run and handed us the streaming callback.
|
|
721
|
+
await waitFor(() => {
|
|
722
|
+
expect(updateContent).toBeDefined();
|
|
723
|
+
expect(deferredResolve).toBeDefined();
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
// Stream content through the callback while the hook promise is still pending.
|
|
727
|
+
updateContent?.({
|
|
728
|
+
asset: {
|
|
729
|
+
id: "next-label-action",
|
|
730
|
+
type: "action",
|
|
731
|
+
value: "streamed value",
|
|
732
|
+
},
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
await waitFor(() => {
|
|
736
|
+
view = (player.getState() as InProgressState).controllers.view.currentView
|
|
737
|
+
?.lastUpdate;
|
|
738
|
+
expect(view?.actions[1]?.asset.type).toBe("action");
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
// Now resolve the hook with nothing. The streamed content must survive.
|
|
742
|
+
deferredResolve?.(undefined);
|
|
743
|
+
|
|
744
|
+
await waitFor(() => {
|
|
745
|
+
view = (player.getState() as InProgressState).controllers.view.currentView
|
|
746
|
+
?.lastUpdate;
|
|
747
|
+
expect(view?.actions[1]?.asset.type).toBe("action");
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
view = (player.getState() as InProgressState).controllers.view.currentView
|
|
751
|
+
?.lastUpdate;
|
|
752
|
+
expect(view?.actions[0].asset.type).toBe("action");
|
|
753
|
+
expect(view?.actions[1]?.asset.type).toBe("action");
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
test("callback-streamed content is not overwritten by a stale hook return value", async () => {
|
|
757
|
+
// Regression: a streaming consumer pushes content through the callback while
|
|
758
|
+
// the hook is still pending, then resolves the hook with a different (stale)
|
|
759
|
+
// value. The callback is the source of truth once used, so the bail return
|
|
760
|
+
// must not overwrite it — otherwise the streamed row is intermittently
|
|
761
|
+
// replaced by the stale content.
|
|
762
|
+
let deferredResolve: ((value: any) => void) | undefined;
|
|
763
|
+
let updateContent: ((content: any) => void) | undefined;
|
|
764
|
+
|
|
765
|
+
const plugin = new AsyncNodePlugin({
|
|
766
|
+
plugins: [new AsyncNodePluginPlugin()],
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
plugin.hooks.onAsyncNode.tap(
|
|
770
|
+
"test",
|
|
771
|
+
async (_node: Node.Async, update: (content: any) => void) => {
|
|
772
|
+
updateContent = update;
|
|
773
|
+
return new Promise((resolve) => {
|
|
774
|
+
deferredResolve = resolve;
|
|
775
|
+
});
|
|
776
|
+
},
|
|
777
|
+
);
|
|
778
|
+
|
|
779
|
+
const player = new Player({ plugins: [plugin] });
|
|
780
|
+
|
|
781
|
+
player.start(basicFRFWithActions as any);
|
|
782
|
+
|
|
783
|
+
await waitFor(() => {
|
|
784
|
+
expect(updateContent).toBeDefined();
|
|
785
|
+
expect(deferredResolve).toBeDefined();
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
// Stream the authoritative content through the callback.
|
|
789
|
+
updateContent?.({
|
|
790
|
+
asset: {
|
|
791
|
+
id: "streamed-action",
|
|
792
|
+
type: "action",
|
|
793
|
+
value: "streamed value",
|
|
794
|
+
},
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
let view: any;
|
|
798
|
+
await waitFor(() => {
|
|
799
|
+
view = (player.getState() as InProgressState).controllers.view.currentView
|
|
800
|
+
?.lastUpdate;
|
|
801
|
+
expect(view?.actions[1]?.asset.id).toBe("streamed-action");
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
// Resolve the hook with a stale, non-null value. It must not win.
|
|
805
|
+
deferredResolve?.({
|
|
806
|
+
asset: {
|
|
807
|
+
id: "stale-action",
|
|
808
|
+
type: "action",
|
|
809
|
+
value: "stale value",
|
|
810
|
+
},
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
await waitFor(() => {
|
|
814
|
+
view = (player.getState() as InProgressState).controllers.view.currentView
|
|
815
|
+
?.lastUpdate;
|
|
816
|
+
expect(view?.actions[1]?.asset.id).toBe("streamed-action");
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
view = (player.getState() as InProgressState).controllers.view.currentView
|
|
820
|
+
?.lastUpdate;
|
|
821
|
+
expect(view?.actions[1]?.asset.id).toBe("streamed-action");
|
|
822
|
+
});
|
|
823
|
+
|
|
687
824
|
test("replaces async nodes with provided node", async () => {
|
|
688
825
|
const plugin = new AsyncNodePlugin({
|
|
689
826
|
plugins: [new AsyncNodePluginPlugin()],
|
package/src/index.ts
CHANGED
|
@@ -315,16 +315,33 @@ export class AsyncNodePluginPlugin implements AsyncNodeViewPlugin {
|
|
|
315
315
|
options: Resolve.NodeResolveOptions,
|
|
316
316
|
) {
|
|
317
317
|
try {
|
|
318
|
+
// Track whether the callback pushed content while the hook was pending;
|
|
319
|
+
// if so it wins over the bail return (avoids the last-writer-wins race
|
|
320
|
+
// that intermittently dropped streamed rows).
|
|
321
|
+
let deliveredViaCallback = false;
|
|
322
|
+
|
|
318
323
|
const result = await this.basePlugin?.hooks.onAsyncNode.call(
|
|
319
324
|
node,
|
|
320
|
-
(
|
|
321
|
-
|
|
325
|
+
(streamedResult) => {
|
|
326
|
+
deliveredViaCallback = true;
|
|
327
|
+
this.parseNodeAndUpdate(
|
|
328
|
+
node,
|
|
329
|
+
context,
|
|
330
|
+
streamedResult,
|
|
331
|
+
options.parseNode,
|
|
332
|
+
);
|
|
322
333
|
},
|
|
323
334
|
);
|
|
324
335
|
|
|
325
336
|
// Stop tracking before the next update is triggered
|
|
326
337
|
context.inProgressNodes.delete(node.id);
|
|
327
|
-
|
|
338
|
+
|
|
339
|
+
// Once the callback has delivered content it is the source of truth, so a
|
|
340
|
+
// stale/empty bail return must not clobber it. Skip unless no callback ran
|
|
341
|
+
// (!deliveredViaCallback) and the return carries content (result != null).
|
|
342
|
+
if (!deliveredViaCallback && result != null) {
|
|
343
|
+
this.parseNodeAndUpdate(node, context, result, options.parseNode);
|
|
344
|
+
}
|
|
328
345
|
} catch (e: unknown) {
|
|
329
346
|
const cause = e instanceof Error ? e : new Error(String(e));
|
|
330
347
|
const playerState = this.basePlugin?.getPlayerInstance()?.getState();
|