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.9.81",
"version": "0.9.82",
"private": true,
"scripts": {
"build": "pnpm -r run build",
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.9.81",
"version": "0.9.82",
"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-samples/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "data-gpu-samples",
"version": "0.9.81",
"version": "0.9.82",
"description": "WebGPU samples built on @adobe/data-gpu",
"type": "module",
"private": true,
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.9.81",
"version": "0.9.82",
"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-tictactoe/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "data-lit-tictactoe",
"version": "0.9.81",
"version": "0.9.82",
"description": "Tic-Tac-Toe sample - Lit web components with @adobe/data-lit and AgenticService",
"type": "module",
"private": true,
Expand Down
2 changes: 1 addition & 1 deletion packages/data-lit-todo/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "data-lit-todo",
"version": "0.9.81",
"version": "0.9.82",
"description": "Todo sample app demonstrating @adobe/data with Lit",
"type": "module",
"private": true,
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.9.81",
"version": "0.9.82",
"description": "Adobe data Lit bindings - hooks, elements, decorators",
"type": "module",
"private": false,
Expand Down
2 changes: 1 addition & 1 deletion packages/data-p2p-tictactoe/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "data-p2p-tictactoe",
"version": "0.9.81",
"version": "0.9.82",
"description": "Serverless P2P tic-tac-toe — WebRTC DataChannel + @adobe/data-sync",
"type": "module",
"private": true,
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.9.81",
"version": "0.9.82",
"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-hello/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "data-react-hello",
"version": "0.9.81",
"version": "0.9.82",
"description": "Hello World sample - click counter using @adobe/data-react",
"type": "module",
"private": true,
Expand Down
2 changes: 1 addition & 1 deletion packages/data-react-pixie/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "data-react-pixie",
"version": "0.9.81",
"version": "0.9.82",
"description": "PixiJS React sample - ECS sprites (bunny, fox) with @adobe/data-react",
"type": "module",
"private": true,
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.9.81",
"version": "0.9.82",
"description": "Adobe data React bindings — hooks and context for ECS database",
"type": "module",
"private": false,
Expand Down
2 changes: 1 addition & 1 deletion packages/data-solid-dashboard/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "data-solid-dashboard",
"version": "0.9.81",
"version": "0.9.82",
"description": "Mini dashboard sample — multiple components sharing one @adobe/data ECS database with SolidJS",
"type": "module",
"private": true,
Expand Down
2 changes: 1 addition & 1 deletion packages/data-solid/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@adobe/data-solid",
"version": "0.9.81",
"version": "0.9.82",
"description": "Adobe data SolidJS bindings — context and provider for ECS database",
"type": "module",
"private": false,
Expand Down
2 changes: 1 addition & 1 deletion packages/data-sync/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@adobe/data-sync",
"version": "0.9.81",
"version": "0.9.82",
"description": "Multi-user real-time synchronisation for @adobe/data ECS — server, client, and in-process loopback.",
"type": "module",
"sideEffects": false,
Expand Down
2 changes: 1 addition & 1 deletion packages/data/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@adobe/data",
"version": "0.9.81",
"version": "0.9.82",
"description": "Adobe data oriented programming library",
"type": "module",
"sideEffects": false,
Expand Down
47 changes: 47 additions & 0 deletions packages/data/src/ecs/database/database-read.type-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,53 @@ function derivePositive() {
type _Head = Assert<Equal<typeof head, Observe<Entity | null>>>;
}

// ============================================================================
// derive — INPUTS overload: a fixed record of external observables is injected
// as CURRENT values (second callback arg), unwrapped `Observe<U> → U`; the first
// callback arg stays the read projection. This is how a derive folds in values
// that don't live in the ECS (e.g. service observables) alongside ECS reads.
// ============================================================================

declare const numberInput: Observe<number>;
declare const stringInput: Observe<string>;

function deriveInputsPositive() {
// inputs are injected as current values, strongly typed by unwrapping each Observe
const sum = db.derive(
{ n: numberInput, s: stringInput },
(d, inputs) => {
type _Inputs = Assert<Equal<typeof inputs, { readonly n: number; readonly s: string }>>;
// the first arg is still the read projection
const r: number = d.resources.frameRate;
return `${inputs.s}:${inputs.n + r}`;
},
);
type _Sum = Assert<Equal<typeof sum, Observe<string>>>;

// a body that mixes an ECS read with an injected input value
const mixed = db.derive(
{ n: numberInput },
(d, inputs) => (d.get(0 as Entity, "x") ?? 0) + inputs.n,
);
type _Mixed = Assert<Equal<typeof mixed, Observe<number>>>;
}

function deriveInputsNegative() {
db.derive({ n: numberInput }, (d, inputs) => {
void d;
// @ts-expect-error `n` unwraps to number, not string
const bad: string = inputs.n;
return bad;
});
db.derive({ n: numberInput }, (d, inputs) => {
void d;
// @ts-expect-error `m` is not an injected input
return inputs.m;
});
// @ts-expect-error an input value must be an Observe, not a bare value
db.derive({ n: 123 }, (d, inputs) => inputs.n);
}

// ============================================================================
// derive — NEGATIVE: everything the read projection removes
// ============================================================================
Expand Down
26 changes: 24 additions & 2 deletions packages/data/src/ecs/database/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,10 +169,32 @@ export interface Database<
* never re-notifies.
*
* The callback receives `Database.Read<this>`, so an *intersection* database
* (e.g. `IndexedCoreDatabase & ResourceComputedDatabase`) resolves to the
* merged read surface — consumers never need to cast.
* resolves to the merged read surface — consumers never need to cast.
*/
derive<T>(compute: (db: Database.Read<this>) => T): Observe<T>;
/**
* `derive` with a fixed record of external `inputs` — arbitrary `Observe<T>`
* values that do NOT live in the ECS (e.g. observables exposed by services).
* Their CURRENT values are injected as the second callback argument, keyed the
* same as `inputs` and unwrapped (`Observe<U> → U`); the first argument stays
* the read projection, so a body can fold external values and ECS reads into
* one synchronous expression.
*
* The inputs are subscribed once, at the root (not read dynamically inside the
* body), so the set is fixed for the life of the derive. The derive recomputes
* when any input emits OR an ECS read it recorded could have changed; like
* {@link Observe.fromProperties}, the first value is withheld until every input
* has produced one. Anything dynamic — an input set that depends on data, or
* an input whose arguments come from an ECS read — composes with
* {@link Observe.withSwitch} / {@link Observe.fromKeys} around the derive.
*/
derive<I extends Record<string, Observe<unknown>>, T>(
inputs: I,
compute: (
db: Database.Read<this>,
inputs: { readonly [K in keyof I]: I[K] extends Observe<infer U> ? U : never },
) => T,
): Observe<T>;
/**
* Wipes all entities and resets all resources to their plugin defaults,
* preserving database identity (observers, transaction wrappers, sync
Expand Down
88 changes: 88 additions & 0 deletions packages/data/src/ecs/database/observe-derive.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { describe, expect, it, beforeEach, vi } from "vitest";
import { Database } from "./database.js";
import { Entity } from "../entity/entity.js";
import { Observe } from "../../observe/index.js";

const createTestDatabase = () =>
Database.create(
Expand Down Expand Up @@ -38,6 +39,25 @@ const createTestDatabase = () =>

const flush = () => new Promise<void>((resolve) => queueMicrotask(() => resolve()));

// A minimal external (non-ECS) observable, standing in for a value a service
// might expose: emits the current value on subscribe, re-emits on `set`.
const makeSource = <T>(initial: T) => {
let value = initial;
const observers = new Set<(v: T) => void>();
const observe: Observe<T> = (notify) => {
observers.add(notify);
notify(value);
return () => {
observers.delete(notify);
};
};
const set = (v: T) => {
value = v;
for (const o of observers) o(v);
};
return { observe, set };
};

describe("db.derive", () => {
let db: ReturnType<typeof createTestDatabase>;
let foo: Entity;
Expand Down Expand Up @@ -177,6 +197,74 @@ describe("db.derive", () => {
unsubscribe();
});

// ── inputs overload: fold external (non-ECS) observables into a derive ──

it("injects external input values and recomputes when either an input or an ECS read changes", () => {
const scale = makeSource(2);
const observer = vi.fn();
// one body reads an ECS value (`a` on foo) AND the injected external input
const unsubscribe = db.derive(
{ scale: scale.observe },
(d, inputs) => (d.get(foo, "a") ?? 0) * inputs.scale,
)(observer);
expect(observer).toHaveBeenLastCalledWith(2); // a=1 * scale=2
db.transactions.setA({ e: foo, a: 5 }); // ECS dep changed
expect(observer).toHaveBeenLastCalledWith(10); // 5 * 2 (latest input reused)
scale.set(3); // external input changed
expect(observer).toHaveBeenLastCalledWith(15); // 5 * 3 (latest ECS read reused)
unsubscribe();
});

it("combines two external inputs with an ECS index read", () => {
db.transactions.makeFoo({ a: 1, b: 0 }); // bucket a=1 now holds 2 entities
const weight = makeSource(10);
const label = makeSource("count");
const observer = vi.fn();
const unsubscribe = db.derive(
{ weight: weight.observe, label: label.observe },
(d, inputs) => `${inputs.label}=${d.indexes.byA.find({ a: 1 }).length * inputs.weight}`,
)(observer);
expect(observer).toHaveBeenLastCalledWith("count=20"); // 2 entities * 10
weight.set(100);
expect(observer).toHaveBeenLastCalledWith("count=200");
label.set("total");
expect(observer).toHaveBeenLastCalledWith("total=200");
unsubscribe();
});

it("withholds the first value until every input has emitted", () => {
// an input that does not emit synchronously on subscribe
let emitInput: ((v: number) => void) | undefined;
const deferred: Observe<number> = (notify) => {
emitInput = notify;
return () => {
emitInput = undefined;
};
};
const observer = vi.fn();
const unsubscribe = db.derive({ d: deferred }, (_db, inputs) => inputs.d + 1)(observer);
expect(observer).not.toHaveBeenCalled(); // no input value yet → nothing computed
emitInput?.(10);
expect(observer).toHaveBeenCalledTimes(1);
expect(observer).toHaveBeenLastCalledWith(11);
unsubscribe();
});

it("structurally dedupes across input changes", () => {
const flag = makeSource(1);
const compute = vi.fn((_db: any, inputs: { flag: number }) => (inputs.flag > 0 ? "pos" : "neg"));
const observer = vi.fn();
const unsubscribe = db.derive({ flag: flag.observe }, compute)(observer);
expect(observer).toHaveBeenCalledTimes(1); // "pos"
flag.set(5); // still positive → recompute runs but result unchanged → no emit
expect(compute).toHaveBeenCalledTimes(2);
expect(observer).toHaveBeenCalledTimes(1);
flag.set(-1); // flips
expect(observer).toHaveBeenCalledTimes(2);
expect(observer).toHaveBeenLastCalledWith("neg");
unsubscribe();
});

it("re-emits on a change to an index bucket it read", async () => {
const observer = vi.fn();
const unsubscribe = db.derive((d) => d.indexes.byA.find({ a: 1 }).length)(observer);
Expand Down
Loading