From 8eb9309334c27a38c0519e7aae27e9f2f1ba1758 Mon Sep 17 00:00:00 2001 From: Doug Martin Date: Tue, 23 Dec 2025 08:19:48 -0500 Subject: [PATCH 1/3] feat: Add pub/sub support [AP-58] This commit introduces a publish-subscribe (pub/sub) mechanism to facilitate communication between managed interactives within the Activity Player. The PubSubManager class handles the creation of channels, publishing messages, and managing subscriptions. --- package-lock.json | 35 ++++++++++--------- package.json | 4 +-- .../managed-interactive/iframe-runtime.tsx | 23 +++++++++++- 3 files changed, 42 insertions(+), 20 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7e48916c..d80954dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,8 +12,8 @@ "@aws-sdk/client-s3": "^3.44.0", "@aws-sdk/s3-request-presigner": "^3.44.0", "@concord-consortium/dynamic-text": "^1.0.6", - "@concord-consortium/interactive-api-host": "^0.8.0", - "@concord-consortium/lara-interactive-api": "^1.10.0", + "@concord-consortium/interactive-api-host": "0.10.0-pre.0", + "@concord-consortium/lara-interactive-api": "1.11.0-pre.0", "@concord-consortium/object-storage": "1.0.0-pre.6", "@concord-consortium/text-decorator": "^1.0.2", "@concord-consortium/token-service": "^2.1.0", @@ -6357,9 +6357,9 @@ } }, "node_modules/@concord-consortium/interactive-api-host": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@concord-consortium/interactive-api-host/-/interactive-api-host-0.8.0.tgz", - "integrity": "sha512-mH9YLYBOIOujr7uKCvKEVFJfyUKqpd4bHKvYvL/8FoE4POuMjG4YmnyNKPs7eBSgD6u07LNl5/8Z/rGollcWRA==", + "version": "0.10.0-pre.0", + "resolved": "https://registry.npmjs.org/@concord-consortium/interactive-api-host/-/interactive-api-host-0.10.0-pre.0.tgz", + "integrity": "sha512-DT11IksqUA1FP0G0UAB42Yaq/gWbEcAE5SpFUpQwbQOO7Fbbazi0wYWez7SyBh2qvPxUgK13nu1MEyRndadfOg==", "dependencies": { "@aws-sdk/client-s3": "^3.27.0", "@aws-sdk/s3-request-presigner": "^3.27.0", @@ -6367,12 +6367,12 @@ } }, "node_modules/@concord-consortium/lara-interactive-api": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@concord-consortium/lara-interactive-api/-/lara-interactive-api-1.10.0.tgz", - "integrity": "sha512-M9fd/cPUODtU4A5kDeBe20enH/RPBO0EePkcKpagTkFVIeZQSWyYtRPQnbOQtN+z9Qvg+r3yfjLuL8urxoAprg==", - "license": "MIT", + "version": "1.11.0-pre.0", + "resolved": "https://registry.npmjs.org/@concord-consortium/lara-interactive-api/-/lara-interactive-api-1.11.0-pre.0.tgz", + "integrity": "sha512-OE+3yMWs9asOJLjAIoQACkcr1xb0QcBUGUTFA8NncgDPWmCXgE5lg+tdglHlBaLBD/LzpDY4J27PX5EAnn9Faw==", "dependencies": { - "iframe-phone": "^1.3.1" + "iframe-phone": "^1.3.1", + "nanoid": "^3.3.7" }, "peerDependencies": { "react": ">=16.9.0", @@ -32329,9 +32329,9 @@ } }, "@concord-consortium/interactive-api-host": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@concord-consortium/interactive-api-host/-/interactive-api-host-0.8.0.tgz", - "integrity": "sha512-mH9YLYBOIOujr7uKCvKEVFJfyUKqpd4bHKvYvL/8FoE4POuMjG4YmnyNKPs7eBSgD6u07LNl5/8Z/rGollcWRA==", + "version": "0.10.0-pre.0", + "resolved": "https://registry.npmjs.org/@concord-consortium/interactive-api-host/-/interactive-api-host-0.10.0-pre.0.tgz", + "integrity": "sha512-DT11IksqUA1FP0G0UAB42Yaq/gWbEcAE5SpFUpQwbQOO7Fbbazi0wYWez7SyBh2qvPxUgK13nu1MEyRndadfOg==", "requires": { "@aws-sdk/client-s3": "^3.27.0", "@aws-sdk/s3-request-presigner": "^3.27.0", @@ -32339,11 +32339,12 @@ } }, "@concord-consortium/lara-interactive-api": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@concord-consortium/lara-interactive-api/-/lara-interactive-api-1.10.0.tgz", - "integrity": "sha512-M9fd/cPUODtU4A5kDeBe20enH/RPBO0EePkcKpagTkFVIeZQSWyYtRPQnbOQtN+z9Qvg+r3yfjLuL8urxoAprg==", + "version": "1.11.0-pre.0", + "resolved": "https://registry.npmjs.org/@concord-consortium/lara-interactive-api/-/lara-interactive-api-1.11.0-pre.0.tgz", + "integrity": "sha512-OE+3yMWs9asOJLjAIoQACkcr1xb0QcBUGUTFA8NncgDPWmCXgE5lg+tdglHlBaLBD/LzpDY4J27PX5EAnn9Faw==", "requires": { - "iframe-phone": "^1.3.1" + "iframe-phone": "^1.3.1", + "nanoid": "^3.3.7" } }, "@concord-consortium/object-storage": { diff --git a/package.json b/package.json index 9bd83806..4e50f372 100644 --- a/package.json +++ b/package.json @@ -151,8 +151,8 @@ "@aws-sdk/client-s3": "^3.44.0", "@aws-sdk/s3-request-presigner": "^3.44.0", "@concord-consortium/dynamic-text": "^1.0.6", - "@concord-consortium/interactive-api-host": "^0.8.0", - "@concord-consortium/lara-interactive-api": "^1.10.0", + "@concord-consortium/interactive-api-host": "0.10.0-pre.0", + "@concord-consortium/lara-interactive-api": "1.11.0-pre.0", "@concord-consortium/object-storage": "1.0.0-pre.6", "@concord-consortium/text-decorator": "^1.0.2", "@concord-consortium/token-service": "^2.1.0", diff --git a/src/components/activity-page/managed-interactive/iframe-runtime.tsx b/src/components/activity-page/managed-interactive/iframe-runtime.tsx index 7e6e7fe1..fdff2364 100644 --- a/src/components/activity-page/managed-interactive/iframe-runtime.tsx +++ b/src/components/activity-page/managed-interactive/iframe-runtime.tsx @@ -8,8 +8,10 @@ import { IGetInteractiveSnapshotResponse, IInitInteractive, ILinkedInteractive, IReportInitInteractive, ISupportedFeatures, ServerMessage, IShowModal, ICloseModal, INavigationOptions, ILinkedInteractiveStateResponse, IAddLinkedInteractiveStateListenerRequest, IRemoveLinkedInteractiveStateListenerRequest, IDecoratedContentEvent, - ITextDecorationInfo, ITextDecorationHandlerInfo, IAttachmentUrlRequest, IAttachmentUrlResponse, IGetInteractiveState, AttachmentInfoMap + ITextDecorationInfo, ITextDecorationHandlerInfo, IAttachmentUrlRequest, IAttachmentUrlResponse, IGetInteractiveState, AttachmentInfoMap, + IPubSubCreateChannel, IPubSubSubscribe, IPubSubUnsubscribe, IPubSubPublish } from "@concord-consortium/lara-interactive-api"; +import { PubSubManager } from "@concord-consortium/interactive-api-host"; import { DynamicTextCustomMessageType, DynamicTextMessage, useDynamicTextContext } from "@concord-consortium/dynamic-text"; import { FirebaseObjectStorageConfig, FirebaseObjectStorageUser } from "@concord-consortium/object-storage"; import Shutterbug from "shutterbug"; @@ -88,6 +90,9 @@ interface IProps { log: (logData: any) => void; } +// this is managed outside of the component to persist across component unmount/mount cycles +const pubSubManager = new PubSubManager(); + export const IframeRuntime: React.ForwardRefExoticComponent = forwardRef((props, ref) => { const { url, id, authoredState, initialInteractiveState, legacyLinkedInteractiveState, setInteractiveState, linkedInteractives, report, proposedHeight, containerWidth, setNewHint, getFirebaseJWT, getAttachmentUrl, showModal, closeModal, setSupportedFeatures, @@ -122,6 +127,8 @@ export const IframeRuntime: React.ForwardRefExoticComponent = forwardRef return; } + pubSubManager.addInteractive(id, phone); + // Just to add some type checking to phone post (ServerMessage). const post = (type: ServerMessage, data: any) => phone.post(type, data); const addListener = (type: ClientMessage, handler: any) => phone.addListener(type, handler); @@ -276,6 +283,18 @@ export const IframeRuntime: React.ForwardRefExoticComponent = forwardRef } } }); + addListener("createChannel", (message: IPubSubCreateChannel) => { + pubSubManager.createChannel(id, message.channelId, message.channelInfo); + }); + addListener("publish", (message: IPubSubPublish) => { + pubSubManager.publish(id, message.channelId, message.message); + }); + addListener("subscribe", (message: IPubSubSubscribe) => { + pubSubManager.subscribe(id, message.channelId, message.subscriptionId); + }); + addListener("unsubscribe", (message: IPubSubUnsubscribe) => { + pubSubManager.unsubscribe(id, message.channelId, message.subscriptionId); + }); // Legacy bug fix: In the 1.0.0 release of the AP the special 'nochange' // message wasn't handled correctly and it was saved as the interactive state @@ -419,6 +438,8 @@ export const IframeRuntime: React.ForwardRefExoticComponent = forwardRef dynamicText.unregisterComponent(componentId); }); + pubSubManager.removeInteractive(id); + if (phoneRef.current) { phoneRef.current.disconnect(); } From 098206ff65b887d9df2d4550f40421d81332dc5c Mon Sep 17 00:00:00 2001 From: Doug Martin Date: Wed, 24 Dec 2025 07:29:04 -0500 Subject: [PATCH 2/3] Updated to final package versions --- package-lock.json | 28 +++++++++---------- package.json | 4 +-- .../managed-interactive/iframe-runtime.tsx | 2 +- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index d80954dd..d8cdb83d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,8 +12,8 @@ "@aws-sdk/client-s3": "^3.44.0", "@aws-sdk/s3-request-presigner": "^3.44.0", "@concord-consortium/dynamic-text": "^1.0.6", - "@concord-consortium/interactive-api-host": "0.10.0-pre.0", - "@concord-consortium/lara-interactive-api": "1.11.0-pre.0", + "@concord-consortium/interactive-api-host": "0.10.0", + "@concord-consortium/lara-interactive-api": "1.11.0", "@concord-consortium/object-storage": "1.0.0-pre.6", "@concord-consortium/text-decorator": "^1.0.2", "@concord-consortium/token-service": "^2.1.0", @@ -6357,9 +6357,9 @@ } }, "node_modules/@concord-consortium/interactive-api-host": { - "version": "0.10.0-pre.0", - "resolved": "https://registry.npmjs.org/@concord-consortium/interactive-api-host/-/interactive-api-host-0.10.0-pre.0.tgz", - "integrity": "sha512-DT11IksqUA1FP0G0UAB42Yaq/gWbEcAE5SpFUpQwbQOO7Fbbazi0wYWez7SyBh2qvPxUgK13nu1MEyRndadfOg==", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@concord-consortium/interactive-api-host/-/interactive-api-host-0.10.0.tgz", + "integrity": "sha512-1vTajPts5QiaJs35ll0seQ+B6eGvhfDQg6mXyX4VOMhB1HqeOmpoEXU5XW/8MRduP0qDCowntT2RcPoukPG9Pw==", "dependencies": { "@aws-sdk/client-s3": "^3.27.0", "@aws-sdk/s3-request-presigner": "^3.27.0", @@ -6367,9 +6367,9 @@ } }, "node_modules/@concord-consortium/lara-interactive-api": { - "version": "1.11.0-pre.0", - "resolved": "https://registry.npmjs.org/@concord-consortium/lara-interactive-api/-/lara-interactive-api-1.11.0-pre.0.tgz", - "integrity": "sha512-OE+3yMWs9asOJLjAIoQACkcr1xb0QcBUGUTFA8NncgDPWmCXgE5lg+tdglHlBaLBD/LzpDY4J27PX5EAnn9Faw==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@concord-consortium/lara-interactive-api/-/lara-interactive-api-1.11.0.tgz", + "integrity": "sha512-3cyXRRjnk8AztAl1MEvRirKtuAvSmAEhH114Z/dJKywDjqGqaW5JBvPwfuoBhuwqG2omwGce5HWUq8ioAaXV9Q==", "dependencies": { "iframe-phone": "^1.3.1", "nanoid": "^3.3.7" @@ -32329,9 +32329,9 @@ } }, "@concord-consortium/interactive-api-host": { - "version": "0.10.0-pre.0", - "resolved": "https://registry.npmjs.org/@concord-consortium/interactive-api-host/-/interactive-api-host-0.10.0-pre.0.tgz", - "integrity": "sha512-DT11IksqUA1FP0G0UAB42Yaq/gWbEcAE5SpFUpQwbQOO7Fbbazi0wYWez7SyBh2qvPxUgK13nu1MEyRndadfOg==", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@concord-consortium/interactive-api-host/-/interactive-api-host-0.10.0.tgz", + "integrity": "sha512-1vTajPts5QiaJs35ll0seQ+B6eGvhfDQg6mXyX4VOMhB1HqeOmpoEXU5XW/8MRduP0qDCowntT2RcPoukPG9Pw==", "requires": { "@aws-sdk/client-s3": "^3.27.0", "@aws-sdk/s3-request-presigner": "^3.27.0", @@ -32339,9 +32339,9 @@ } }, "@concord-consortium/lara-interactive-api": { - "version": "1.11.0-pre.0", - "resolved": "https://registry.npmjs.org/@concord-consortium/lara-interactive-api/-/lara-interactive-api-1.11.0-pre.0.tgz", - "integrity": "sha512-OE+3yMWs9asOJLjAIoQACkcr1xb0QcBUGUTFA8NncgDPWmCXgE5lg+tdglHlBaLBD/LzpDY4J27PX5EAnn9Faw==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@concord-consortium/lara-interactive-api/-/lara-interactive-api-1.11.0.tgz", + "integrity": "sha512-3cyXRRjnk8AztAl1MEvRirKtuAvSmAEhH114Z/dJKywDjqGqaW5JBvPwfuoBhuwqG2omwGce5HWUq8ioAaXV9Q==", "requires": { "iframe-phone": "^1.3.1", "nanoid": "^3.3.7" diff --git a/package.json b/package.json index 4e50f372..9085b3c8 100644 --- a/package.json +++ b/package.json @@ -151,8 +151,8 @@ "@aws-sdk/client-s3": "^3.44.0", "@aws-sdk/s3-request-presigner": "^3.44.0", "@concord-consortium/dynamic-text": "^1.0.6", - "@concord-consortium/interactive-api-host": "0.10.0-pre.0", - "@concord-consortium/lara-interactive-api": "1.11.0-pre.0", + "@concord-consortium/interactive-api-host": "0.10.0", + "@concord-consortium/lara-interactive-api": "1.11.0", "@concord-consortium/object-storage": "1.0.0-pre.6", "@concord-consortium/text-decorator": "^1.0.2", "@concord-consortium/token-service": "^2.1.0", diff --git a/src/components/activity-page/managed-interactive/iframe-runtime.tsx b/src/components/activity-page/managed-interactive/iframe-runtime.tsx index fdff2364..37999758 100644 --- a/src/components/activity-page/managed-interactive/iframe-runtime.tsx +++ b/src/components/activity-page/managed-interactive/iframe-runtime.tsx @@ -284,7 +284,7 @@ export const IframeRuntime: React.ForwardRefExoticComponent = forwardRef } }); addListener("createChannel", (message: IPubSubCreateChannel) => { - pubSubManager.createChannel(id, message.channelId, message.channelInfo); + pubSubManager.createChannel(message.channelId, message.channelInfo); }); addListener("publish", (message: IPubSubPublish) => { pubSubManager.publish(id, message.channelId, message.message); From a8da8a2cc0c1b2610c1699710b43a035ab17b229 Mon Sep 17 00:00:00 2001 From: Doug Martin Date: Wed, 24 Dec 2025 07:47:37 -0500 Subject: [PATCH 3/3] Added PubSubManager mock to test --- .../managed-interactive.test.tsx | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/components/activity-page/managed-interactive/managed-interactive.test.tsx b/src/components/activity-page/managed-interactive/managed-interactive.test.tsx index 20bbb5a8..7a669050 100644 --- a/src/components/activity-page/managed-interactive/managed-interactive.test.tsx +++ b/src/components/activity-page/managed-interactive/managed-interactive.test.tsx @@ -74,9 +74,37 @@ jest.mock("@concord-consortium/token-service", () => ({ })); const mockHandleGetAttachmentUrl = jest.fn(); -jest.mock("@concord-consortium/interactive-api-host", () => ({ - handleGetAttachmentUrl: (...args: any) => mockHandleGetAttachmentUrl(...args) -})); +let mockAddInteractive: jest.Mock; +let mockRemoveInteractive: jest.Mock; +let mockCreateChannel: jest.Mock; +let mockSubscribe: jest.Mock; +let mockUnsubscribe: jest.Mock; +let mockPublish: jest.Mock; + +jest.mock("@concord-consortium/interactive-api-host", () => { + // Initialize the mock functions + mockAddInteractive = jest.fn(); + mockRemoveInteractive = jest.fn(); + mockCreateChannel = jest.fn(); + mockSubscribe = jest.fn(); + mockUnsubscribe = jest.fn(); + mockPublish = jest.fn(); + + // Create a mock class that returns an instance with all the methods + class MockPubSubManager { + addInteractive = mockAddInteractive; + removeInteractive = mockRemoveInteractive; + createChannel = mockCreateChannel; + subscribe = mockSubscribe; + unsubscribe = mockUnsubscribe; + publish = mockPublish; + } + + return { + handleGetAttachmentUrl: (...args: any) => mockHandleGetAttachmentUrl(...args), + PubSubManager: MockPubSubManager + }; +}); const mockWatchAnswer = jest.fn((id: string, callback: (answer: any) => void) => callback({ meta: {} })); jest.mock("../../../firebase-db", () => ({