From cb8492815d5f76d14a9dc06b63bb6a6cd81d1da5 Mon Sep 17 00:00:00 2001 From: Brian Dahlem Date: Fri, 8 May 2026 19:30:30 +0000 Subject: [PATCH 1/4] add proposal for presentation-local activity communication channel --- ...line-activity-session-services-proposal.md | 522 ++++++++++++++++++ 1 file changed, 522 insertions(+) create mode 100644 .agent/plans/syncdeck-inline-activity-session-services-proposal.md diff --git a/.agent/plans/syncdeck-inline-activity-session-services-proposal.md b/.agent/plans/syncdeck-inline-activity-session-services-proposal.md new file mode 100644 index 00000000..e430e2dc --- /dev/null +++ b/.agent/plans/syncdeck-inline-activity-session-services-proposal.md @@ -0,0 +1,522 @@ +# Proposal: Presentation-Local Activity Session Services For SyncDeck + +## Summary + +SyncDeck already supports embedded ActiveBits activities by letting Reveal decks declare launch intent with `data-activity-*` attributes. The host then owns child-session lifecycle, session creation, participant handoff, and activity launch. + +This proposal is for a different class of interaction: activities authored directly inside the presentation itself. Deck-local JavaScript should be able to opt into narrowly scoped SyncDeck session services without becoming an ActiveBits activity, creating a child session, or depending on the ActiveBits activity registry. + +The core abstraction is an isolated service channel. A presentation-owned widget can declare a channel for a specific local activity instance, and SyncDeck can relay or persist only the state that belongs to that parent session and channel. + +The goal is to support a spectrum of classroom interactions: + +- local-only student activities that survive reloads, if the host later supports private restore state +- shared activities where students exchange or submit events +- instructor-visible progress and checkpoints +- instructor-controlled shared state such as timers, phases, random seeds, or released challenges +- eventual dashboard views for activity progress and shared classroom state + +The motivating first service is a synchronized timer. When the instructor starts a deck-local timer, all viewers should derive the same remaining time from canonical SyncDeck state. The timer should not become an embedded ActiveBits activity just to synchronize. + +## Motivation + +Course decks increasingly include JavaScript interactions directly in the presentation. For example, a cybersecurity deck might include a public/private key lab where students drag keys, encrypt messages, verify signatures, and receive step-by-step feedback. + +Some of these interactions should remain private and local to each student. Others would benefit from session awareness: + +- a timer should start, pause, resume, and finish at the same time for instructor and students +- an instructor might broadcast a challenge prompt to all students +- students might submit an encrypted or signed response +- a paired activity might allow students to exchange public keys or messages +- the instructor might want aggregate progress such as "24 of 29 students completed step 2" +- late joiners or refreshed browsers should restore the current activity state + +Without a shared SyncDeck service layer, each deck-local activity must either remain isolated or invent its own messaging scheme. That creates duplicated logic and inconsistent behavior across courses. + +## Non-Goals + +- Do not create ActiveBits child sessions. +- Do not route students into `/activity`, `/manage`, waiting-room, or child-session entry flows. +- Do not register presentation-local activities in the ActiveBits activity registry. +- Do not multiplex full ActiveBits activity websocket traffic through SyncDeck. +- Do not give deck-authored code direct access to raw SyncDeck session records. +- Do not use `session.data.embeddedActivities` for presentation-local activity state. + +## Relationship To Embedded Activities + +This proposal does not replace the existing embedded ActiveBits activity flow. + +Embedded ActiveBits activities should continue to use: + +- `data-activity-id` +- `data-activity-trigger` +- `data-activity-options` +- `activityRequest` +- host-owned launch and child session lifecycle +- child activity websocket and report routes + +Presentation-local activity services are for interactions that live inside the Reveal deck itself. They are useful when the activity is small, tightly coupled to slide content, or only needs narrow host services such as shared timers, state replay, or progress aggregation. + +The two systems should remain parallel: + +- Embedded ActiveBits activity: registered app, child session, activity-owned runtime. +- Presentation-local activity: deck-owned code, parent session service channel, SyncDeck-owned synchronization. + +A deck-local timer should not be represented as an embedded child session, and an embedded ActiveBits activity should not depend on the presentation-local service channel. + +## Design Principles + +1. Decks describe intent; the host owns orchestration. +2. Presentation-local activity APIs should be generic and activity-agnostic. +3. Activities must continue to work in standalone Reveal preview mode. +4. Local student state and shared classroom state should be clearly separate. +5. Timers and other shared state should sync canonical state, not high-frequency UI updates. +6. The protocol should use stable local identity based on `localActivityId`, `instanceKey`, and optional slide `location`. +7. The host should decide routing, persistence, permissions, and dashboard display. +8. Existing embedded activity contracts should remain unchanged. + +## Proposed Concept + +Add a presentation-local activity service layer inside SyncDeck with isolated channels. Each channel belongs to one parent SyncDeck session and one deck-local activity instance. + +Suggested persisted namespace: + +```ts +session.data.presentationLocalActivities = { + [channelKey: string]: { + localActivityId: string + instanceKey: string + location?: { h: number; v: number; f?: number } + updatedAt: number + sharedState?: Record + timer?: { + status: 'idle' | 'running' | 'paused' + durationMs: number + startedAt: number | null + pausedAt: number | null + elapsedBeforePauseMs: number + } + } +} +``` + +`channelKey` should be derived by the server from normalized inputs rather than trusted directly from the iframe. A good starting shape is: + +```ts +`${localActivityId}:${instanceKey}` +``` + +This namespace is intentionally separate from `session.data.embeddedActivities`. + +Add a browser API exposed by the SyncDeck Reveal runtime: + +```js +window.SyncDeckSession +``` + +This API would sit above the existing `RevealIframeSyncAPI.sendCustom(...)` mechanism and provide deck authors with small, structured services. + +At a high level: + +- presentation-local activities register themselves with the host +- activities can store local state +- activities can emit progress or shared events +- activities can subscribe to host-relayed events or state updates +- shared utilities, especially timers, can be controlled by the instructor and rendered consistently across all participants + +## Example Authoring API + +### Register A Presentation-Local Activity + +```js +const keyLab = SyncDeckSession.activities.register({ + localActivityId: 'public-private-key-lab', + instanceKey: 'public-private-key-lab:6:0', + title: 'Public/Private Key Lab', + visibility: 'instructor', + capabilities: ['localState', 'progress', 'sharedEvents'] +}); +``` + +The runtime should derive useful defaults when possible: + +- `location` from `Reveal.getIndices()` +- `instanceKey` from `localActivityId:h:v` if not supplied +- role/session support from the SyncDeck host handshake + +### Local Student State + +```js +keyLab.local.set({ + selectedStep: 'decrypt-response', + completedSteps: ['encrypt-message'], + attempts: { + 'encrypt-message': 2 + } +}); + +keyLab.local.onRestore((state) => { + restoreKeyLabUi(state); +}); +``` + +Local state is private to the student by default. The host may persist it for restore, but it should not be broadcast to classmates. + +### Progress Events + +```js +keyLab.progress.emit({ + checkpoint: 'encrypt-message', + status: 'complete' +}); +``` + +The host can aggregate this for instructor-facing views. + +### Shared Activity Events + +```js +keyLab.events.send('message-submitted', { + ciphertext: 'Q7F2-19AC', + signed: true +}); + +keyLab.events.on('challenge-broadcast', (event) => { + renderChallenge(event.payload); +}); +``` + +The host decides whether events go to: + +- instructor only +- all students +- a group +- a specific partner +- no one, if the current role is not allowed + +### Shared Timers + +```js +const timer = SyncDeckSession.timers.get('discussion-timer'); + +timer.start({ + durationMs: 180000, + label: 'Partner discussion' +}); + +timer.onChange((state) => { + renderTimer(state); +}); +``` + +Timer state should sync canonical timestamps rather than remaining seconds: + +```json +{ + "id": "discussion-timer", + "status": "running", + "startedAt": 1778240000000, + "durationMs": 180000, + "pausedAt": null, + "elapsedBeforePauseMs": 0, + "serverNow": 1778240060000 +} +``` + +Each iframe computes display time locally from the canonical state. This keeps students aligned without sending updates every second. + +Student-originated timer control should be rejected unless a future capability explicitly allows student-owned channels. + +## Proposed Message Types + +These would be host-facing messages sent through the existing `reveal-sync` envelope. + +### `presentationLocalActivity:declare` + +Sent when a presentation-local activity declares an isolated channel. + +```json +{ + "type": "reveal-sync", + "action": "presentationLocalActivity:declare", + "deckId": "protecting-data", + "role": "student", + "payload": { + "localActivityId": "public-private-key-lab", + "instanceKey": "public-private-key-lab:6:0", + "title": "Public/Private Key Lab", + "location": { "h": 6, "v": 0, "f": -1 }, + "capabilities": ["localState", "progress", "sharedEvents"] + } +} +``` + +### `presentationLocalActivity:event` + +Sent when deck-local code emits an event. + +```json +{ + "type": "reveal-sync", + "action": "presentationLocalActivity:event", + "deckId": "protecting-data", + "role": "student", + "payload": { + "localActivityId": "public-private-key-lab", + "instanceKey": "public-private-key-lab:6:0", + "event": "message-submitted", + "scope": "instructor", + "payload": { + "ciphertext": "Q7F2-19AC", + "signed": true + } + } +} +``` + +### `presentationLocalActivity:localState` + +Sent when a student activity updates private restore state. + +```json +{ + "type": "reveal-sync", + "action": "presentationLocalActivity:localState", + "deckId": "protecting-data", + "role": "student", + "payload": { + "localActivityId": "public-private-key-lab", + "instanceKey": "public-private-key-lab:6:0", + "state": { + "completedSteps": ["encrypt-message"] + } + } +} +``` + +### `presentationLocalActivity:sharedStateSet` + +Sent when authorized deck code requests an update to shared state for its isolated channel. + +```json +{ + "type": "reveal-sync", + "action": "presentationLocalActivity:sharedStateSet", + "deckId": "protecting-data", + "role": "instructor", + "payload": { + "localActivityId": "discussion-tools", + "instanceKey": "discussion-tools:4:0", + "key": "timer:discussion-timer", + "value": { + "status": "running", + "startedAt": 1778240000000, + "durationMs": 180000, + "pausedAt": null, + "elapsedBeforePauseMs": 0 + } + } +} +``` + +### `presentationLocalActivity:sharedState` + +Sent by the host to iframes when shared state changes or when a participant joins late. + +```json +{ + "type": "reveal-sync", + "action": "command", + "payload": { + "name": "presentationLocalActivity:sharedState", + "localActivityId": "discussion-tools", + "instanceKey": "discussion-tools:4:0", + "key": "timer:discussion-timer", + "value": { + "status": "running", + "startedAt": 1778240000000, + "durationMs": 180000, + "pausedAt": null, + "elapsedBeforePauseMs": 0, + "serverNow": 1778240060000 + } + } +} +``` + +## Permissions And Routing + +The host should enforce routing and mutation rules. Suggested defaults: + +| Capability | Instructor | Student | +| --- | --- | --- | +| Declare local activity channel | yes | yes | +| Write local private state | yes | yes | +| Emit progress | yes | yes | +| Read aggregate progress | yes | no | +| Start/pause/reset shared timer | yes | no, unless delegated | +| Read shared timer | yes | yes | +| Broadcast event to all | yes | no, unless delegated | +| Submit event to instructor | yes | yes | +| Send peer/group event | configurable | configurable | + +Deck authors should be able to request a scope, but the host should be authoritative. + +Suggested event scopes: + +- `local` +- `instructor` +- `all` +- `group` +- `partner` +- `host` + +## Privacy And Safety + +Presentation-local activities may collect student work or behavior. The API should make data classification explicit: + +- local private state +- progress metadata +- student submission +- shared classroom state +- instructor broadcast + +The host should avoid broadcasting private local state. Student submissions should be routed according to host policy, and dashboard displays should distinguish aggregate progress from visible student work. + +The iframe is deck-authored code, so messages are still untrusted input. The server should validate and bound every payload before persistence or broadcast. + +The host should enforce: + +- maximum channels per SyncDeck session +- maximum serialized state size per channel +- maximum timer duration +- maximum string lengths for ids and state fields +- explicit rejection of unknown capabilities or unauthorized scopes +- pruning for stale channels if needed + +## Websocket Behavior + +SyncDeck parent websocket remains the transport for presentation-local service events because the activity lives inside the presentation iframe and does not have its own ActiveBits session. + +This is different from embedded ActiveBits activities, where the parent websocket carries lifecycle envelopes and the child activity owns its realtime connection. + +Expected behavior: + +- instructor command arrives through the manager host +- server validates role and channel identity +- server persists canonical channel state +- server broadcasts the channel update to connected SyncDeck viewers +- manager and student hosts forward the scoped update into their presentation iframes +- new or reconnecting viewers receive replayed channel state after SyncDeck websocket authentication or registration + +## Storage And Cleanup + +Presentation-local activity state should live and expire with the parent SyncDeck session. Ending a SyncDeck session removes the local channel state automatically because it is part of the parent session data. + +No child-session TTL coupling is needed. + +## Standalone Behavior + +Decks must still run outside SyncDeck, including direct browser preview and VS Code preview. + +In standalone mode: + +- `SyncDeckSession` may exist as a no-op/local-only shim +- local state can use in-memory storage or browser storage if enabled by the deck +- shared events should not throw errors +- timers can run locally +- methods should return predictable results such as `{ supported: false }` or resolve without network effects + +Example: + +```js +if (!SyncDeckSession.supported) { + startLocalTimer(); +} +``` + +## Candidate Use Cases + +### Synchronized Timers + +Deck-authored timers can be controlled by the instructor and displayed consistently across instructor and student views. + +Important implementation detail: sync timer state, not per-second ticks. + +### Public/Private Key Lab + +Local mode: + +- student drags keys +- student sees local feedback +- local progress restores after reload + +Shared mode: + +- instructor sees aggregate progress +- instructor broadcasts a message to encrypt or verify +- students submit encrypted or signed artifacts +- optional partner mode exchanges public keys or challenge messages + +### Warm-Up And Exit Ticket Widgets + +For quick prompts that do not need the full embedded Resonance activity flow, deck-local code can collect lightweight status or short responses. + +### Student-Controlled Simulations + +Students can manipulate local parameters while the instructor sees completion or checkpoint signals. + +### Released Challenge Phases + +The instructor can move a class through shared phases: + +- setup +- attempt +- discuss +- reveal +- debrief + +Each phase is shared state, and deck UI can respond to phase changes. + +## Implementation Roadmap + +### Phase 1: Instructor-Owned Broadcast Channels And Timers + +- Add shared SyncDeck types for presentation-local activity channel messages. +- Add a `presentationLocalActivities` normalizer to SyncDeck session data. +- Add server helpers to normalize `localActivityId`, `instanceKey`, timer state, and state patches. +- Add instructor-only websocket handling for shared timer commands. +- Broadcast and replay timer state to instructor and student sockets. +- Add manager/student host code that forwards scoped channel updates into the Reveal iframe. +- Provide a standalone no-op/local shim. +- Add tests for timer start/replay, rejected student mutation, payload limits, session isolation, and no interaction with `embeddedActivities`. + +### Phase 2: Local State And Progress + +- Add student-private local restore state if persistence is needed. +- Add progress event ingestion with aggregate instructor views. +- Keep raw student work and aggregate progress as separate data classes. + +### Phase 3: Optional Event Routing + +- Add controlled event scopes such as `instructor`, `all`, `group`, or `partner`. +- Add explicit capability gates before students can broadcast or peer-route events. +- Revisit reporting and scoring only after the channel model is stable. + +## Open Questions + +- Should deck-local activities be allowed to create student-private state in phase one, or should phase one be instructor-owned broadcast state only? +- Should shared state updates be full replacement, shallow patch, or operation-based? +- Should channel declarations be required before updates, or can updates implicitly create channels? +- Should the deck runtime expose one generic channel API, or separate helpers such as `createSyncedTimer(...)`? +- How much of this should be available in standalone presentation preview outside a live SyncDeck session? +- Should channel identity include a deck or presentation identifier beyond the parent SyncDeck session and `instanceKey`? + +## Definition Of Done + +- Presentation-local activity service state is clearly separate from embedded ActiveBits activity state. +- A deck-local timer can be started by the instructor and stays synchronized across manager and student presentation iframes. +- Late joiners receive current timer state. +- Student-originated instructor-only mutations are rejected. +- Payload limits and validation prevent deck-authored messages from writing arbitrary or unbounded session data. +- Existing embedded activity behavior and tests continue to pass. From a145049e8c1df80e03fb83be0697f83c9aae9fca Mon Sep 17 00:00:00 2001 From: Brian Dahlem Date: Fri, 8 May 2026 19:41:26 +0000 Subject: [PATCH 2/4] update proposal --- ...line-activity-session-services-proposal.md | 120 +++++++++++------- 1 file changed, 76 insertions(+), 44 deletions(-) diff --git a/.agent/plans/syncdeck-inline-activity-session-services-proposal.md b/.agent/plans/syncdeck-inline-activity-session-services-proposal.md index e430e2dc..4066d741 100644 --- a/.agent/plans/syncdeck-inline-activity-session-services-proposal.md +++ b/.agent/plans/syncdeck-inline-activity-session-services-proposal.md @@ -13,10 +13,10 @@ The goal is to support a spectrum of classroom interactions: - local-only student activities that survive reloads, if the host later supports private restore state - shared activities where students exchange or submit events - instructor-visible progress and checkpoints -- instructor-controlled shared state such as timers, phases, random seeds, or released challenges +- instructor-controlled shared state such as phases, random seeds, released challenges, prompts, or timers - eventual dashboard views for activity progress and shared classroom state -The motivating first service is a synchronized timer. When the instructor starts a deck-local timer, all viewers should derive the same remaining time from canonical SyncDeck state. The timer should not become an embedded ActiveBits activity just to synchronize. +The motivating first slice is a generic service channel with one concrete reference service. A synchronized timer is a useful proof case, but the architecture should work the same way for phases, prompts, random seeds, lightweight pairing, or other presentation-owned classroom coordination. ## Motivation @@ -70,7 +70,7 @@ A deck-local timer should not be represented as an embedded child session, and a 2. Presentation-local activity APIs should be generic and activity-agnostic. 3. Activities must continue to work in standalone Reveal preview mode. 4. Local student state and shared classroom state should be clearly separate. -5. Timers and other shared state should sync canonical state, not high-frequency UI updates. +5. Shared services should sync canonical state, not high-frequency UI updates. 6. The protocol should use stable local identity based on `localActivityId`, `instanceKey`, and optional slide `location`. 7. The host should decide routing, persistence, permissions, and dashboard display. 8. Existing embedded activity contracts should remain unchanged. @@ -89,13 +89,11 @@ session.data.presentationLocalActivities = { location?: { h: number; v: number; f?: number } updatedAt: number sharedState?: Record - timer?: { - status: 'idle' | 'running' | 'paused' - durationMs: number - startedAt: number | null - pausedAt: number | null - elapsedBeforePauseMs: number - } + services?: Record + updatedAt: number + }> } } ``` @@ -108,6 +106,19 @@ session.data.presentationLocalActivities = { This namespace is intentionally separate from `session.data.embeddedActivities`. +Student-private restore state should not live beside shared channel state. If SyncDeck later persists local student state, use a separate per-student namespace so replay paths cannot accidentally broadcast private work: + +```ts +session.data.presentationLocalStudentState = { + [studentId: string]: { + [channelKey: string]: { + updatedAt: number + localState: Record + } + } +} +``` + Add a browser API exposed by the SyncDeck Reveal runtime: ```js @@ -122,7 +133,7 @@ At a high level: - activities can store local state - activities can emit progress or shared events - activities can subscribe to host-relayed events or state updates -- shared utilities, especially timers, can be controlled by the instructor and rendered consistently across all participants +- shared utilities can be controlled by the instructor and rendered consistently across all participants ## Example Authoring API @@ -146,6 +157,8 @@ The runtime should derive useful defaults when possible: ### Local Student State +Student-private restore state is a future capability, not a phase-one requirement. It is included here to show the intended API direction and to keep private state separate from shared channel state from the beginning. + ```js keyLab.local.set({ selectedStep: 'decrypt-response', @@ -194,10 +207,19 @@ The host decides whether events go to: - a specific partner - no one, if the current role is not allowed -### Shared Timers +### Shared Service Commands + +Presentation-local activities should request service actions; SyncDeck should produce canonical state. This keeps deck-authored code from writing host-owned timestamps, phase versions, or authoritative counters directly. ```js -const timer = SyncDeckSession.timers.get('discussion-timer'); +const discussionTools = SyncDeckSession.activities.register({ + localActivityId: 'discussion-tools', + instanceKey: 'discussion-tools:4:0', + title: 'Discussion Tools', + capabilities: ['sharedState', 'services'] +}); + +const timer = discussionTools.timers.get('discussion-timer'); timer.start({ durationMs: 180000, @@ -209,7 +231,7 @@ timer.onChange((state) => { }); ``` -Timer state should sync canonical timestamps rather than remaining seconds: +For a timer service, the deck requests `start` with a duration. SyncDeck stamps canonical server-owned fields such as `startedAt`, then broadcasts state: ```json { @@ -223,14 +245,16 @@ Timer state should sync canonical timestamps rather than remaining seconds: } ``` -Each iframe computes display time locally from the canonical state. This keeps students aligned without sending updates every second. +Each iframe computes display time locally from canonical state. Clients should use `serverNow` to establish a server-clock offset instead of relying on raw local `Date.now()` alone. This keeps students aligned without sending updates every second. -Student-originated timer control should be rejected unless a future capability explicitly allows student-owned channels. +The same command/state pattern can support other services later, such as released phases, random seeds, classroom prompts, progress checkpoints, or lightweight pairing. Student-originated service commands should be rejected unless a future capability explicitly allows student-owned channels. ## Proposed Message Types These would be host-facing messages sent through the existing `reveal-sync` envelope. +Fields such as `deckId` and `role` in iframe-originated examples are descriptive only. SyncDeck must derive the authoritative session, role, student identity, and instructor authority from the host/websocket context. + ### `presentationLocalActivity:declare` Sent when a presentation-local activity declares an isolated channel. @@ -294,45 +318,46 @@ Sent when a student activity updates private restore state. } ``` -### `presentationLocalActivity:sharedStateSet` +### `presentationLocalActivity:serviceCommand` -Sent when authorized deck code requests an update to shared state for its isolated channel. +Sent when authorized deck code requests a service action for its isolated channel. The iframe sends intent; SyncDeck validates the request, applies policy, and creates canonical state. ```json { "type": "reveal-sync", - "action": "presentationLocalActivity:sharedStateSet", + "action": "presentationLocalActivity:serviceCommand", "deckId": "protecting-data", "role": "instructor", "payload": { "localActivityId": "discussion-tools", "instanceKey": "discussion-tools:4:0", - "key": "timer:discussion-timer", - "value": { - "status": "running", - "startedAt": 1778240000000, - "durationMs": 180000, - "pausedAt": null, - "elapsedBeforePauseMs": 0 + "serviceId": "discussion-timer", + "serviceType": "timer", + "command": "start", + "params": { + "durationMs": 180000 } } } ``` -### `presentationLocalActivity:sharedState` +### `presentationLocalActivity:serviceState` + +Sent by the host to iframes when service state changes or when a participant joins late. -Sent by the host to iframes when shared state changes or when a participant joins late. +The Reveal runtime should intercept and dispatch this command to `SyncDeckSession` subscribers before the generic iframe-sync command handler treats it as an unknown deck command. ```json { "type": "reveal-sync", "action": "command", "payload": { - "name": "presentationLocalActivity:sharedState", + "name": "presentationLocalActivity:serviceState", "localActivityId": "discussion-tools", "instanceKey": "discussion-tools:4:0", - "key": "timer:discussion-timer", - "value": { + "serviceId": "discussion-timer", + "serviceType": "timer", + "state": { "status": "running", "startedAt": 1778240000000, "durationMs": 180000, @@ -354,8 +379,8 @@ The host should enforce routing and mutation rules. Suggested defaults: | Write local private state | yes | yes | | Emit progress | yes | yes | | Read aggregate progress | yes | no | -| Start/pause/reset shared timer | yes | no, unless delegated | -| Read shared timer | yes | yes | +| Send instructor-owned service command | yes | no, unless delegated | +| Read shared service state | yes | yes | | Broadcast event to all | yes | no, unless delegated | | Submit event to instructor | yes | yes | | Send peer/group event | configurable | configurable | @@ -389,7 +414,8 @@ The host should enforce: - maximum channels per SyncDeck session - maximum serialized state size per channel -- maximum timer duration +- maximum service command parameter sizes +- service-specific bounds, such as maximum timer duration - maximum string lengths for ids and state fields - explicit rejection of unknown capabilities or unauthorized scopes - pruning for stale channels if needed @@ -409,6 +435,8 @@ Expected behavior: - manager and student hosts forward the scoped update into their presentation iframes - new or reconnecting viewers receive replayed channel state after SyncDeck websocket authentication or registration +Iframe readiness should be handled explicitly. SyncDeck hosts should cache the latest canonical state per channel locally after websocket replay, then forward it only after the presentation iframe is ready and `SyncDeckSession` has initialized. If a local activity declares its channel after replay has already happened, the host should replay the matching cached state to that iframe on declaration. This avoids dropping state when the parent websocket connects before the deck runtime finishes loading. + ## Storage And Cleanup Presentation-local activity state should live and expire with the parent SyncDeck session. Ending a SyncDeck session removes the local channel state automatically because it is part of the parent session data. @@ -424,7 +452,7 @@ In standalone mode: - `SyncDeckSession` may exist as a no-op/local-only shim - local state can use in-memory storage or browser storage if enabled by the deck - shared events should not throw errors -- timers can run locally +- services such as timers can run locally - methods should return predictable results such as `{ supported: false }` or resolve without network effects Example: @@ -443,6 +471,8 @@ Deck-authored timers can be controlled by the instructor and displayed consisten Important implementation detail: sync timer state, not per-second ticks. +Timers are one example of the generic service command/canonical state pattern, not the center of the architecture. + ### Public/Private Key Lab Local mode: @@ -480,16 +510,17 @@ Each phase is shared state, and deck UI can respond to phase changes. ## Implementation Roadmap -### Phase 1: Instructor-Owned Broadcast Channels And Timers +### Phase 1: Instructor-Owned Service Channels - Add shared SyncDeck types for presentation-local activity channel messages. - Add a `presentationLocalActivities` normalizer to SyncDeck session data. -- Add server helpers to normalize `localActivityId`, `instanceKey`, timer state, and state patches. -- Add instructor-only websocket handling for shared timer commands. -- Broadcast and replay timer state to instructor and student sockets. +- Add server helpers to normalize `localActivityId`, `instanceKey`, service commands, and canonical service state. +- Add instructor-only websocket handling for shared service commands. +- Broadcast and replay canonical service state to instructor and student sockets. - Add manager/student host code that forwards scoped channel updates into the Reveal iframe. - Provide a standalone no-op/local shim. -- Add tests for timer start/replay, rejected student mutation, payload limits, session isolation, and no interaction with `embeddedActivities`. +- Add one reference service, likely a synchronized timer, to prove command/state semantics. +- Add tests for command handling, replay, rejected student mutation, payload limits, session isolation, iframe-ready caching, and no interaction with `embeddedActivities`. ### Phase 2: Local State And Progress @@ -506,17 +537,18 @@ Each phase is shared state, and deck UI can respond to phase changes. ## Open Questions - Should deck-local activities be allowed to create student-private state in phase one, or should phase one be instructor-owned broadcast state only? -- Should shared state updates be full replacement, shallow patch, or operation-based? +- Should generic shared state updates be full replacement, shallow patch, or operation-based? - Should channel declarations be required before updates, or can updates implicitly create channels? -- Should the deck runtime expose one generic channel API, or separate helpers such as `createSyncedTimer(...)`? +- Should the deck runtime expose one generic channel API, or separate service helpers such as `createSyncedTimer(...)`? - How much of this should be available in standalone presentation preview outside a live SyncDeck session? - Should channel identity include a deck or presentation identifier beyond the parent SyncDeck session and `instanceKey`? ## Definition Of Done - Presentation-local activity service state is clearly separate from embedded ActiveBits activity state. -- A deck-local timer can be started by the instructor and stays synchronized across manager and student presentation iframes. -- Late joiners receive current timer state. +- A reference service can be controlled by the instructor and stays synchronized across manager and student presentation iframes. +- Late joiners receive current service state. +- Iframe-ready and late-declare replay paths deliver current channel state reliably. - Student-originated instructor-only mutations are rejected. - Payload limits and validation prevent deck-authored messages from writing arbitrary or unbounded session data. - Existing embedded activity behavior and tests continue to pass. From 5d94232901073344ff5dfdfc4e7da48913ed65d8 Mon Sep 17 00:00:00 2001 From: Brian Dahlem Date: Fri, 8 May 2026 19:45:32 +0000 Subject: [PATCH 3/4] update proposal --- ...cdeck-inline-activity-session-services-proposal.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.agent/plans/syncdeck-inline-activity-session-services-proposal.md b/.agent/plans/syncdeck-inline-activity-session-services-proposal.md index 4066d741..ca13c8f5 100644 --- a/.agent/plans/syncdeck-inline-activity-session-services-proposal.md +++ b/.agent/plans/syncdeck-inline-activity-session-services-proposal.md @@ -88,6 +88,7 @@ session.data.presentationLocalActivities = { instanceKey: string location?: { h: number; v: number; f?: number } updatedAt: number + // Optional future generic state escape hatch. Phase one should prefer typed services. sharedState?: Record services?: Record @@ -130,8 +133,8 @@ This API would sit above the existing `RevealIframeSyncAPI.sendCustom(...)` mech At a high level: - presentation-local activities register themselves with the host -- activities can store local state -- activities can emit progress or shared events +- activities may eventually store local private state +- activities may eventually emit progress or shared events - activities can subscribe to host-relayed events or state updates - shared utilities can be controlled by the instructor and rendered consistently across all participants @@ -139,6 +142,8 @@ At a high level: ### Register A Presentation-Local Activity +This is a future/full API example. Phase one can start narrower with declaration plus instructor-owned service commands. + ```js const keyLab = SyncDeckSession.activities.register({ localActivityId: 'public-private-key-lab', From 19754a47615068ecc6ed56a228f379da8181b0fe Mon Sep 17 00:00:00 2001 From: Brian Dahlem Date: Fri, 8 May 2026 19:56:15 +0000 Subject: [PATCH 4/4] added implementation plan --- ...line-activity-session-services-proposal.md | 158 ++++++++++++++---- 1 file changed, 121 insertions(+), 37 deletions(-) diff --git a/.agent/plans/syncdeck-inline-activity-session-services-proposal.md b/.agent/plans/syncdeck-inline-activity-session-services-proposal.md index ca13c8f5..6998baa1 100644 --- a/.agent/plans/syncdeck-inline-activity-session-services-proposal.md +++ b/.agent/plans/syncdeck-inline-activity-session-services-proposal.md @@ -513,47 +513,131 @@ The instructor can move a class through shared phases: Each phase is shared state, and deck UI can respond to phase changes. -## Implementation Roadmap - -### Phase 1: Instructor-Owned Service Channels - -- Add shared SyncDeck types for presentation-local activity channel messages. -- Add a `presentationLocalActivities` normalizer to SyncDeck session data. -- Add server helpers to normalize `localActivityId`, `instanceKey`, service commands, and canonical service state. -- Add instructor-only websocket handling for shared service commands. -- Broadcast and replay canonical service state to instructor and student sockets. -- Add manager/student host code that forwards scoped channel updates into the Reveal iframe. -- Provide a standalone no-op/local shim. -- Add one reference service, likely a synchronized timer, to prove command/state semantics. -- Add tests for command handling, replay, rejected student mutation, payload limits, session isolation, iframe-ready caching, and no interaction with `embeddedActivities`. - -### Phase 2: Local State And Progress - -- Add student-private local restore state if persistence is needed. -- Add progress event ingestion with aggregate instructor views. -- Keep raw student work and aggregate progress as separate data classes. - -### Phase 3: Optional Event Routing - -- Add controlled event scopes such as `instructor`, `all`, `group`, or `partner`. -- Add explicit capability gates before students can broadcast or peer-route events. -- Revisit reporting and scoring only after the channel model is stable. +## Implementation Plan + +### Ownership Callout + +Keep implementation authority split deliberately: + +- SyncDeck host owns session authority, role/participant validation, websocket handling, persistence, canonical service state, replay, payload limits, and iframe forwarding. +- The shared `syncdeck-reveal` runtime library imported by presentation HTML owns the deck-author-facing `window.SyncDeckSession` API, local subscription dispatch, Reveal-derived defaults, convenience service helpers, and standalone/no-host preview behavior. +- Shared protocol types should live in one versioned contract that both sides can consume or mirror safely. Older decks and older hosts should fail gracefully when presentation-local services are unavailable. + +### Phase 0: Contract Lock + +- [ ] Define shared TypeScript types for presentation-local activity channels, service commands, service state, and host-to-iframe replay messages. +- [ ] Put the types in a SyncDeck-owned shared module, likely under `activities/syncdeck/shared/`, so server, manager, student, and runtime helpers use one contract. +- [ ] Add pure validators/normalizers for `localActivityId`, `instanceKey`, `location`, `serviceId`, `serviceType`, `command`, and command `params`. +- [ ] Define hard limits before wiring runtime behavior: + - maximum channels per SyncDeck session + - maximum services per channel + - maximum serialized params/state size + - maximum identifier lengths + - service-specific limits for the reference service +- [ ] Decide whether phase one requires explicit channel declaration before commands. Recommended: require declaration for clearer replay and authorization semantics. + +Acceptance checks: + +- Types compile in both client and server imports. +- Normalizer tests cover malformed ids, overlong ids, invalid locations, unknown service types, oversized params, and valid command payloads. +- Contract docs in this proposal remain aligned with exported type names. + +### Phase 1: Server Session State And Command Handling + +- [ ] Extend SyncDeck session data normalization with `presentationLocalActivities`. +- [ ] Keep the new namespace independent from `embeddedActivities`; do not create or read child sessions. +- [ ] Add server helpers to create/update channel records by derived `channelKey`. +- [ ] Add instructor-only handling for `presentationLocalActivity:serviceCommand` on the existing SyncDeck parent websocket path. +- [ ] Treat iframe-supplied `deckId` and `role` as descriptive only; derive authority from the authenticated SyncDeck socket context. +- [ ] Implement one reference service using the generic command/state pattern. A synchronized timer is the likely first service because it proves canonical server time, replay, and client-side derivation. +- [ ] Persist canonical service state under `presentationLocalActivities[channelKey].services[serviceId]`. +- [ ] Broadcast host-authored `presentationLocalActivity:serviceState` messages to connected SyncDeck viewers. +- [ ] Replay canonical service state to newly authenticated instructors and newly registered students. + +Acceptance checks: + +- Instructor command creates or updates only the requested parent-session channel. +- Student-originated instructor-owned service commands are rejected. +- Service command params are bounded and sanitized before persistence. +- Server-authored state includes server-owned fields for the reference service. +- Ending the parent SyncDeck session removes presentation-local state with normal parent session deletion. + +### Phase 2: Manager And Student Host Forwarding + +- [ ] Add manager-side handling for presentation-local iframe messages from the Reveal deck. +- [ ] Add student-side handling for declaration and supported read-only presentation-local messages. +- [ ] Forward valid iframe-originated service commands from manager host to the SyncDeck websocket using the existing instructor-authenticated connection. +- [ ] Maintain a local host cache of latest canonical service state by `channelKey + serviceId`. +- [ ] Forward host-authored service state into the Reveal iframe only after iframe ready and `SyncDeckSession` runtime initialization. +- [ ] On late channel declaration, replay matching cached state into the iframe. +- [ ] Ensure cached state is scoped to the current SyncDeck session and cleared on session/presentation changes. + +Acceptance checks: + +- State replay is not lost when websocket replay arrives before iframe ready. +- State replay is not lost when the iframe declares a channel after host replay. +- Student host receives service state but cannot issue instructor-owned commands. +- Manager and student hosts do not interpret presentation-local messages as embedded activity launch requests. + +### Phase 3: Presentation Runtime Shim + +- [ ] Add a small `window.SyncDeckSession` runtime helper for decks. +- [ ] Support channel declaration and subscription to service state. +- [ ] Support generic service command helpers, with a convenience wrapper for the reference service if useful. +- [ ] Provide standalone behavior for direct Reveal preview: + - declaration succeeds locally + - service subscriptions do not throw + - reference service can run locally when SyncDeck is unavailable + - unsupported host-backed operations return predictable `{ supported: false }`-style results +- [ ] Document how deck authors should use the runtime without depending on ActiveBits internals. + +Acceptance checks: + +- Deck-local code can declare a channel in SyncDeck and standalone preview. +- A reference service can be driven through the runtime API. +- Unknown host messages do not break existing Reveal sync command handling. + +### Phase 4: Reference Flow And Tests + +- [ ] Add a small deck-local reference fixture or dev presentation section that uses the service API. +- [ ] Exercise one instructor-owned command and service-state replay path. +- [ ] Add server route/websocket tests in `activities/syncdeck/server/routes.test.ts` or extracted helper tests. +- [ ] Add client helper tests for manager/student host replay and iframe-ready caching. +- [ ] Add runtime helper tests if the runtime lives in testable TypeScript. +- [ ] Keep tests focused on presentation-local channels and include negative assertions that `embeddedActivities` is unchanged. + +Validation targets: + +- Focused SyncDeck server tests for command handling, auth rejection, replay, and payload limits. +- Focused SyncDeck manager/student tests for iframe-ready replay behavior. +- Activities workspace lint/typecheck for touched SyncDeck files. +- Broader `npm test` before merge when environment allows. +- If browser-level presentation load behavior changes, add or run the shared Playwright harness through `npm run test:e2e`. + +### Future Phases + +- [ ] Student-private restore state under `presentationLocalStudentState`. +- [ ] Progress event ingestion and aggregate instructor views. +- [ ] Controlled event routing for `instructor`, `all`, `group`, or `partner` scopes. +- [ ] Presentation-owned report blocks or score contributions, only after the channel model stabilizes. +- [ ] Optional dashboard surfacing for active presentation-local channels. ## Open Questions -- Should deck-local activities be allowed to create student-private state in phase one, or should phase one be instructor-owned broadcast state only? -- Should generic shared state updates be full replacement, shallow patch, or operation-based? -- Should channel declarations be required before updates, or can updates implicitly create channels? -- Should the deck runtime expose one generic channel API, or separate service helpers such as `createSyncedTimer(...)`? -- How much of this should be available in standalone presentation preview outside a live SyncDeck session? +- Should phase one require explicit channel declaration before any service command? Recommended: yes. +- Should generic shared state remain only a future escape hatch, or should it be removed until a concrete non-service use case appears? +- Should service storage keys remain `serviceId`, or should implementation immediately use `${serviceType}:${serviceId}`? +- Should the deck runtime expose only one generic service API in phase one, or include a reference convenience helper such as `createSyncedTimer(...)`? +- How much standalone-preview behavior should be implemented in the same branch as host-backed service channels? - Should channel identity include a deck or presentation identifier beyond the parent SyncDeck session and `instanceKey`? ## Definition Of Done -- Presentation-local activity service state is clearly separate from embedded ActiveBits activity state. -- A reference service can be controlled by the instructor and stays synchronized across manager and student presentation iframes. -- Late joiners receive current service state. -- Iframe-ready and late-declare replay paths deliver current channel state reliably. -- Student-originated instructor-only mutations are rejected. -- Payload limits and validation prevent deck-authored messages from writing arbitrary or unbounded session data. -- Existing embedded activity behavior and tests continue to pass. +- [ ] Presentation-local activity service state is clearly separate from embedded ActiveBits activity state. +- [ ] Phase-one implementation uses generic service command/state contracts, with any reference timer treated as one service implementation. +- [ ] A reference service can be controlled by the instructor and stays synchronized across manager and student presentation iframes. +- [ ] Late joiners receive current service state. +- [ ] Iframe-ready and late-declare replay paths deliver current channel state reliably. +- [ ] Student-originated instructor-only mutations are rejected. +- [ ] Payload limits and validation prevent deck-authored messages from writing arbitrary or unbounded session data. +- [ ] Existing embedded activity behavior and tests continue to pass.