Skip to content

Adds presence to the p2p tictactoe sample.#103

Open
krisnye wants to merge 18 commits intomainfrom
knye/presence
Open

Adds presence to the p2p tictactoe sample.#103
krisnye wants to merge 18 commits intomainfrom
knye/presence

Conversation

@krisnye
Copy link
Copy Markdown
Collaborator

@krisnye krisnye commented May 10, 2026

Description

Related Issue

Motivation and Context

How Has This Been Tested?

Screenshots (if appropriate):

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • I have signed the Adobe Open Source CLA.
  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.
  • All new and existing tests passed.

krisnye and others added 18 commits May 9, 2026 20:38
…bserve hook

- Add `usePointerObserve` to @adobe/data-lit: returns Observe<PointerPosition>
  from pointermove events on the host element. Use with
  `Observe.toAsyncGenerator(pos, () => false)` for a never-ending presence
  stream — the data-lit equivalent of the missing 'use-drag-generator' pattern.

- Add `movePresence` transaction + `cursorX`/`cursorO` resources to the
  tictactoe ECS plugin. Fixed envelope IDs (PRESENCE_ID.X / .O) ensure each
  mouse move replaces the previous cursor entry in the reconciling DB's
  transient queue rather than accumulating.

- Rewrite game-view.ts with a persistent DOM scaffold so the pointermove
  listener and cursor overlay (.board-wrap / .cursors) survive game-state
  re-renders. Remote player's cursor rendered as a labeled dot via
  sendTransient → ReconcilingDB.apply(time=-1) → observe → DOM update.

- Add ARCHITECTURE.md with mermaid diagrams: current sync flow, presence
  data flow, and a diagram showing how persistence could be layered on top
  without touching the sync code.

Co-authored-by: Cursor <cursoragent@cursor.com>
…rker shim

Co-authored-by: Cursor <cursoragent@cursor.com>
Full restructure to follow the same patterns as data-lit-tictactoe:
- @CustomElement classes, element + presentation + css separation
- P2pElement base class (service + syncClient + myMark + withHooks)
- useObservableValues for all reactive reads (board, firstPlayer, cursors)
- usePointerObserve + Observe.toAsyncGenerator(() => false) in p2p-board
  for the never-ending presence stream (the "drag sequence" pattern)
- sendTransient called from the async generator loop in useEffect
- p2p-app is a plain LitElement managing the signaling state machine
  via @State reactive properties
- Delete game-view.ts, ui.ts, game-state.ts (vanilla DOM approach)
- Add lit + @adobe/data-lit dependencies; fix tsconfig (experimentalDecorators)

Co-authored-by: Cursor <cursoragent@cursor.com>
…ta-lit

Remove the hand-rolled base class and use the standard DatabaseElement
from @adobe/data-lit as the foundation. DatabaseElement already:
- owns the service property
- wires withHooks on render()
- propagates service via DOM ancestor traversal

P2pElement just adds syncClient and myMark on top.
p2p-app stays a plain LitElement with @State service so child elements
can find it via ancestor traversal — matching how data-lit-tictactoe works.

Co-authored-by: Cursor <cursoragent@cursor.com>
…State

Phase, offerCode, answerCode, bannerText, bannerError, myMark, and
syncClient are now ephemeral ECS resources in p2pPlugin. Transactions
(startHostSignaling, startJoinSignaling, setOfferCode, setAnswerCode,
setBanner, connected) are applied directly to the local DB — never
through syncClient.propose — so they never reach the remote peer.

P2pApp now extends P2pElement (DatabaseElement), gets its service for
free via ancestor traversal, and reads phase/offerCode/etc. reactively
via useObservableValues. No @State, no Phase type, no prop drilling.

P2pElement replaces @Property syncClient/myMark with plain getters that
read from this.service.resources, so all child elements continue to work
unchanged and the game render template needs zero property bindings.

Co-authored-by: Cursor <cursoragent@cursor.com>
…gotiation

## @adobe/data — core changes

- Add `TransactionContext<C,R,A>` type (extends Store + `readonly userId`)
- Thread `userId` through `execute()` options in TransactionalStore and
  ObservedDatabase so every transaction function sees `t.userId`
- ReconcilingDatabase passes `entry.userId` / `envelope.userId` to each
  execute call; removes no-op transient entries from the queue immediately
- Dispatcher suppresses `observe.envelopes` notification for no-op
  transactions (empty redo/undo); `hasTransient` only set when effective
- New tests: `t.userId` for local/sync/inbound cases; no-op suppression

## data-lit-tictactoe — library extraction

- Gate `playMove` by `t.userId`: only the current player's mark may move;
  undefined userId (standalone/AI) allows all moves
- Retype `TictactoeElement` on `tictactoePlugin` (not `agentPlugin`); child
  elements only need the base surface; injecting an agent DB still satisfies
- `main.ts` creates `agentPlugin` DB explicitly and injects it before mount
- Add `src/index.ts` barrel + `exports` map so workspace consumers can
  `import { tictactoePlugin, tictactoeTagName } from "data-lit-tictactoe"`

## data-p2p-tictactoe — generic negotiation architecture

- Delete all game-coupled elements: p2p-board/cell/hud, p2p-element,
  game-plugin, p2p-plugin, duplicated board-state/player-mark/presence types
- `negotiationPlugin`: remove `myMark` resource; `connected()` takes no args
- New `presencePlugin`: `cursorX`, `cursorO` resources + `movePresence`
  transaction keyed by `t.userId`; lives outside data-lit-tictactoe
- New `<p2p-negotiation>`: game-agnostic element with injectable `gamePlugin`,
  `gameTagName`, `assignUserId`, optional `presenceTagName`; owns its own
  local-only negotiation DB; on WebRTC connect creates the synced game DB and
  mounts the game element (optionally wrapped in presence overlay)
- New `<p2p-presence-overlay>`: wraps game via `<slot>`, renders cursor dots,
  drives `movePresence` via `usePointerObserve` + async-generator pattern
- `<p2p-app>`: thin shell combining `tictactoePlugin + presencePlugin`,
  wiring `assignUserId` role→mark, delegating all logic to `<p2p-negotiation>`
- Update ARCHITECTURE.md with two-database model, new mermaid diagrams
- Update data-sync README with `t.userId` and no-op replication sections

Co-authored-by: Cursor <cursoragent@cursor.com>
Add two always-active Claude rules that capture the locality-of-knowledge
and namespace-folder patterns (.claude/rules/data-modelling.md,
.claude/rules/namespace.md), then apply them across both tictactoe
packages.

PlayerMark namespace gains four helpers — values, is, markColor, opponent
— so every literal "X" / "O" outside types/player-mark/ disappears:
- presence overlay iterates PlayerMark.values and reads markColor,
  dropping the .cursor--x / .cursor--o CSS rules and the call-site
  toLowerCase() derivation
- presence plugin switches to Partial<Record<PlayerMark, Vec2>> with a
  PlayerMark.is guard at the boundary
- restartGame and currentPlayer use PlayerMark.opponent
- defaults use PlayerMark.values[0] instead of "X"
- tictactoe-cell narrows with PlayerMark.is(cell)
- p2p-app's role-to-mark mapping uses PlayerMark.values index

Rules were validated by running parallel subagent proposals on a fresh
domain (audit Severity) and on this case; both converged independently.

Co-authored-by: Cursor <cursoragent@cursor.com>
…te cast

Transaction wrappers previously typed the AsyncArgsProvider path as
returning the same `void | Entity` as the sync path, which silently lied
about the dispatcher's runtime behaviour (it actually returns Promise<R>
when given a Promise/AsyncGenerator factory). Consumers that wanted to
await or .catch() the async path were forced to cast.

Replace the union signature with a true overload — async-provider first
so it wins selection when Input is itself function-shaped — and add a
dedicated transaction-functions.type-test.ts covering both green paths
(plain → R, async factory → Promise<R>, .catch() compiles without cast)
and red paths (.catch on plain result, wrong arg shapes, no-input
positional args). Existing Equal<...> assertions in database-schema and
create-database-schema-test were strengthened to match the overload.

The single call-site cast in p2p-presence-overlay drops out naturally.

Co-authored-by: Cursor <cursoragent@cursor.com>
Captures the principle reinforced by the recent ToTransactionFunctions
overload fix: `as` asserts a runtime invariant the compiler can't see;
it is never the right fix for a declaration that lies. When a cast is
the only thing letting consumer code compile, fix the source
declaration so the cast disappears for every caller.

Co-authored-by: Cursor <cursoragent@cursor.com>
Distil firefly-platform's binding-element / lit / presentation rules
into two lean, framework-agnostic rules with no proprietary content:

- element.md (~80 lines, scoped to **/*element*) — container element
  discipline: thin wire between data service and presentation, raw
  subscription reads, one-line action callbacks, lifecycle through
  hooks, deletion test as the load-bearing heuristic.
- presentation.md (~60 lines, scoped to **/*presentation.ts) — pure
  function from props to renderable output, only render and
  unlocalized exports, verbNoun callback names.

Per-framework details (Lit / React / Solid) live in a small table at
the bottom of each rule rather than scattered through the prose, so
the principles read identically regardless of the rendering library.

Co-authored-by: Cursor <cursoragent@cursor.com>
The unlocalized bundle pattern is proprietary and Adobe-internal; the
presentation rule should describe only the framework-agnostic surface.
Render is now the single permitted export.

Co-authored-by: Cursor <cursoragent@cursor.com>
The namespace pattern (eponymous folder + file, public.ts barrel,
single import surface) applies equally to capability bundles
(SyncService.create, SyncService.Options) as it does to data types,
not only to files under types/. Loosen the auto-attach glob so the
rule surfaces wherever a namespace folder might live.

Co-authored-by: Cursor <cursoragent@cursor.com>
…esence filter

data/ecs:
- Add Database.reset() across core/store/observed/reconciling layers
  O(archetypes+resources), preserves database identity and observers
- Add tests: red/green equivalence, observer fan-out, reconciler queue clear

data-sync:
- Add hello/welcome handshake protocol: sessionId, watermark-based replay
  (full vs tail-only), resetRequired flag, onWelcome callback
- Add SyncService.sessionId() and lastAppliedTime() for reconnect watermarks
- Add ping/pong keep-alive: configurable pingIntervalMs (10s) and
  livenessTimeoutMs (25s) on both client and server; closes transport on
  timeout so onClose → reconnect flow triggers automatically
- Add opt-in logger option to createSyncService and createSyncServer
- Add onClose to SyncTransport interface; implement in loopback, WebSocket
- Update README: connection resilience section covering hello/welcome,
  keep-alive, liveness, logger, and updated API reference tables
- Tests: transport-onclose, keepalive (8 cases with fake timers), reconnect

data-p2p-tictactoe:
- Refactor signaling.ts to pre-negotiate two data channels (sync id=0,
  signal id=1) using negotiated:true; return pc + signalChannel
- Add renegotiator.ts: ICE restart over the signal channel; host triggers
  restartIce on pc.connectionState===disconnected, trickles candidates,
  joiner applies offer/answer — heals path changes without re-signaling
- negotiation-controller.ts: wire renegotiator, add reconnect() to
  NegotiationController, add connection/role/sessionId resources to
  negotiationPlugin, show disconnected banner with reconnect button
- Remove error-event-as-close from WebRTC transports (was causing false
  disconnects); remove loopback onClose from wireHostSync (internal infra)
- Wire SyncService/SyncServer logger to console for dev visibility
- Fix priorSessionId/initialWatermark capture before dispose() in wireHostSync

data-lit-tictactoe:
- Add xWins/oWins/draws resources to tictactoePlugin; tally in restartGame
- Update HUD element, presentation, and CSS to display score pill
- Improve status text: "X's turn" / "X wins!" / "Draw!"

data-p2p-tictactoe (presence):
- Filter local player mark from presence overlay; peer cursors only

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant