Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "data-monorepo",
"version": "0.10.9",
"version": "0.10.10",
"private": true,
"engines": {
"node": ">=24"
Expand Down
2 changes: 1 addition & 1 deletion packages/data-ai/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "adobe-data-ai",
"version": "0.10.9",
"version": "0.10.10",
"description": "Architecture skills for @adobe/data — data-oriented modelling, archetype iteration, hot-path performance, and related conventions.",
"author": {
"name": "Adobe"
Expand Down
2 changes: 1 addition & 1 deletion packages/data-ai/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@adobe/data-ai",
"version": "0.10.9",
"version": "0.10.10",
"description": "Cross-agent architecture skills for @adobe/data — installable as a Claude Code plugin or copied into any Agent-Skills-compatible agent (Cursor, Codex).",
"type": "module",
"private": false,
Expand Down
2 changes: 1 addition & 1 deletion packages/data-gpu/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@adobe/data-gpu",
"version": "0.10.9",
"version": "0.10.10",
"description": "Adobe data WebGPU plugins and types for graphics and compute",
"type": "module",
"private": false,
Expand Down
2 changes: 1 addition & 1 deletion packages/data-lit/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@adobe/data-lit",
"version": "0.10.9",
"version": "0.10.10",
"description": "Adobe data Lit bindings - hooks, elements, decorators",
"type": "module",
"private": false,
Expand Down
2 changes: 1 addition & 1 deletion packages/data-persistence/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@adobe/data-persistence",
"version": "0.10.9",
"version": "0.10.10",
"description": "Worker-based incremental persistence layer for @adobe/data ECS over OPFS (browser) and node:fs (server).",
"type": "module",
"sideEffects": false,
Expand Down
2 changes: 1 addition & 1 deletion packages/data-react/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@adobe/data-react",
"version": "0.10.9",
"version": "0.10.10",
"description": "Adobe data React bindings — hooks and context for ECS database",
"type": "module",
"private": false,
Expand Down
43 changes: 43 additions & 0 deletions packages/data-rpc-iframe/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# data-rpc-iframe

A runnable sample for [`@adobe/data-rpc`](../data-rpc): a **main frame** embeds a
real **iframe** (the sub frame), and the two exchange async data services over a
single `MessagePort` — **in both directions**.

## Run it

```bash
pnpm --filter data-rpc-iframe dev
```

Then open http://localhost:3011/. The left column is the main frame; the right
column is the sub frame (a genuine `<iframe>`).

## What it demonstrates

Each side both **exposes** its own service and **consumes** the peer's, so every
communication kind runs in both directions:

| kind | main → sub | sub → main |
| --- | --- | --- |
| **observe** | sub renders `main.time` live | main renders `sub.status` live |
| **promise** | `sub.echo(msg)` | `main.echo(msg)` |
| **generator** | `sub.countUp(5)` (pull-based) | `main.countUp(5)` |
| **void** | `main.notify(msg)` → sub's inbox | `sub.log(msg)` → main's logs |

It also shows a **nested sub-service**: `sub.calc` (a child service with
`total`/`add`/`reset`) is shimmed recursively and driven from the main frame —
`sub.calc.add(5)` (nested promise), `sub.calc.total` (nested observe), and
`sub.calc.reset()` (nested void).

## How the wiring works

1. `main.ts` creates a `MessageChannel`, embeds `sub.html`, and on the iframe's
`load` posts `port2` to it via `window.postMessage` (same-origin).
2. Each side wraps its port with `createMessagePortTransport`, creates a
`createRpcEndpoint`, then `expose`s its service and `consume`s the peer's.
3. The Lit panels render reactively from the observed service state via
`@adobe/data-lit`'s `useObservableValues`.

The service contracts live in `src/shared/`; the per-frame implementations and
panels live in `src/main/` and `src/sub/`.
25 changes: 25 additions & 0 deletions packages/data-rpc-iframe/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@adobe/data-rpc — iframe sample</title>
<style>
body { margin: 0; font-family: system-ui, sans-serif; padding: 1rem; }
h1 { font-size: 1.25rem; }
#layout { display: flex; gap: 1rem; align-items: flex-start; }
#app { flex: 1 1 0; --accent: #4a6; }
#frame-container { flex: 1 1 0; }
</style>
</head>
<body>
<h1>@adobe/data-rpc — bidirectional services across an iframe</h1>
<p>Left = main frame. Right = sub frame (a real iframe). Each drives the other's
observe / promise / generator / void members over one MessagePort.</p>
<div id="layout">
<div id="app"></div>
<div id="frame-container"></div>
</div>
<script type="module" src="/src/main/main.ts"></script>
</body>
</html>
29 changes: 29 additions & 0 deletions packages/data-rpc-iframe/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "data-rpc-iframe",
"version": "0.10.9",
"description": "Sample: bidirectional @adobe/data-rpc services across a main frame and an iframe, over a MessagePort.",
"type": "module",
"private": true,
"exports": {
".": "./src/main/main.ts"
},
"scripts": {
"build": "vite build",
"dev": "vite",
"test": "vitest --run --passWithNoTests",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@adobe/data": "workspace:*",
"@adobe/data-lit": "workspace:*",
"@adobe/data-rpc": "workspace:*",
"lit": "^3.3.1"
},
"devDependencies": {
"jsdom": "^24.1.0",
"typescript": "^5.8.3",
"vite": "^5.1.1",
"vite-plugin-checker": "^0.12.0",
"vitest": "^1.6.0"
}
}
32 changes: 32 additions & 0 deletions packages/data-rpc-iframe/src/main/create-main-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// © 2026 Adobe. MIT License. See /LICENSE for details.

import { Observe } from "@adobe/data/observe";
import { MainService } from "../shared/main-service.js";

const delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));

/** The MAIN frame's local implementation of {@link MainService}. */
export function createMainService(): MainService {
const [time, setTime] = Observe.createState(0);
const [logs, setLogs] = Observe.createState<readonly string[]>([]);

let ticks = 0;
setInterval(() => setTime((ticks += 1)), 1000);

let received: readonly string[] = [];

return {
serviceName: "main",
schema: Observe.fromConstant(MainService.schema),
time,
logs,
echo: async (message) => `main echoes: ${message}`,
countUp: async function* (to) {
for (let i = 1; i <= to; i++) {
await delay(300);
yield i;
}
},
log: (message) => setLogs((received = [...received, message])),
};
}
91 changes: 91 additions & 0 deletions packages/data-rpc-iframe/src/main/main-panel-element.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// © 2026 Adobe. MIT License. See /LICENSE for details.

import { html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { useObservableValues } from "@adobe/data-lit";
import { RpcPanelElement } from "../ui/rpc-panel-element.js";
import { panelStyles } from "../ui/panel.css.js";
import type { MainService } from "../shared/main-service.js";
import type { SubService } from "../shared/sub-service.js";

const tagName = "main-panel";

declare global {
interface HTMLElementTagNameMap {
[tagName]: MainPanelElement;
}
}

/**
* The MAIN frame's panel. Observes its own `time`/`logs` (local) and the sub's
* `status` (remote), and drives the sub's promise / generator / void members.
*/
@customElement(tagName)
export class MainPanelElement extends RpcPanelElement {
static styles = panelStyles;

@property({ attribute: false }) local!: MainService;
@property({ attribute: false }) remote!: SubService;

@state() private echoResult = "";
@state() private streamOut: readonly number[] = [];
@state() private calcResult = "";

render() {
const v = useObservableValues(
() => ({
time: this.local.time,
logs: this.local.logs,
status: this.remote.status,
calcTotal: this.remote.calc.total, // NESTED observe across the boundary
}),
[this.local, this.remote],
);
return html`
<h2>Main frame</h2>
<div class="row"><span class="label">Clock (local):</span><span class="value">${v?.time ?? 0}</span></div>
<div class="row"><span class="label">Sub status (observe):</span><span class="value">${v?.status ?? "…"}</span></div>
<div class="row"><span class="label">Logs from sub (void):</span></div>
<ul>${(v?.logs ?? []).map((m) => html`<li>${m}</li>`)}</ul>
<div class="row">
<button @click=${this.doEcho}>echo → sub (promise)</button>
<span class="value">${this.echoResult}</span>
</div>
<div class="row">
<button @click=${this.doStream}>countUp ← sub (generator)</button>
<span class="value">${this.streamOut.join(" ")}</span>
</div>
<div class="row">
<button @click=${this.doNotify}>notify → sub (void)</button>
</div>
<div class="row"><span class="label">sub.calc.total (nested observe):</span><span class="value">${v?.calcTotal ?? 0}</span></div>
<div class="row">
<button @click=${this.doCalcAdd}>calc.add(5) → sub (nested promise)</button>
<span class="value">${this.calcResult}</span>
<button @click=${this.doCalcReset}>calc.reset → sub (nested void)</button>
</div>
`;
}

private doEcho = async () => {
this.echoResult = await this.remote.echo(`hi from main @${new Date().toLocaleTimeString()}`);
};

private doStream = async () => {
this.streamOut = [];
for await (const n of this.remote.countUp(5)) this.streamOut = [...this.streamOut, n];
};

private doNotify = () => {
this.remote.notify(`hello from main @${new Date().toLocaleTimeString()}`);
};

private doCalcAdd = async () => {
this.calcResult = `= ${await this.remote.calc.add(5)}`;
};

private doCalcReset = () => {
this.remote.calc.reset();
this.calcResult = "";
};
}
11 changes: 11 additions & 0 deletions packages/data-rpc-iframe/src/main/main-panel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// © 2026 Adobe. MIT License. See /LICENSE for details.

import { html, type TemplateResult } from "lit";
import type { MainService } from "../shared/main-service.js";
import type { SubService } from "../shared/sub-service.js";

/** Lazy wrapper: imports the element module, then renders it with its services. */
export const MainPanel = (args: { local: MainService; remote: SubService }): TemplateResult => {
void import("./main-panel-element.js");
return html`<main-panel .local=${args.local} .remote=${args.remote}></main-panel>`;
};
44 changes: 44 additions & 0 deletions packages/data-rpc-iframe/src/main/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// © 2026 Adobe. MIT License. See /LICENSE for details.

import { render } from "lit";
import { createRpcEndpoint, createMessagePortTransport } from "@adobe/data-rpc";
import { MainService } from "../shared/main-service.js";
import { SubService } from "../shared/sub-service.js";
import { HANDSHAKE, type HandshakeMessage } from "../shared/handshake.js";
import { createMainService } from "./create-main-service.js";
import { MainPanel } from "./main-panel.js";

// Bootstrap the MAIN frame: embed the sub iframe, hand it one end of a
// MessageChannel, then expose the main service and consume the sub service over
// the other end. No top-level await (see repo CLAUDE.md) — all work is synchronous
// wiring plus event callbacks.
function init(): void {
const app = document.getElementById("app");
const frameContainer = document.getElementById("frame-container");
if (app === null || frameContainer === null) return;

const channel = new MessageChannel();

const iframe = document.createElement("iframe");
iframe.src = "/sub.html";
iframe.title = "sub frame";
iframe.style.cssText = "width: 100%; min-height: 360px; border: 0;";
iframe.addEventListener(
"load",
() => {
// Hand port2 to the sub. targetOrigin is this page's origin (same-origin sample).
iframe.contentWindow?.postMessage({ type: HANDSHAKE } satisfies HandshakeMessage, location.origin, [channel.port2]);
},
{ once: true },
);
frameContainer.appendChild(iframe);

const endpoint = createRpcEndpoint(createMessagePortTransport(channel.port1));
const mainService = createMainService();
endpoint.expose("main", mainService, MainService.schema);
const subService = endpoint.consume("sub", SubService.schema);

render(MainPanel({ local: mainService, remote: subService }), app);
}

init();
Loading