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.26",
"version": "0.10.27",
"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.26",
"version": "0.10.27",
"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.26",
"version": "0.10.27",
"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.26",
"version": "0.10.27",
"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.26",
"version": "0.10.27",
"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.26",
"version": "0.10.27",
"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.26",
"version": "0.10.27",
"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-rpc/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@adobe/data-rpc",
"version": "0.10.26",
"version": "0.10.27",
"description": "Schema-driven, bidirectional projection of @adobe/data async data services across a boundary (iframe / MessagePort / Worker). Only Data crosses the wire.",
"type": "module",
"sideEffects": false,
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.10.26",
"version": "0.10.27",
"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.10.26",
"version": "0.10.27",
"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-testing/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@adobe/data-testing",
"version": "0.10.26",
"version": "0.10.27",
"description": "Conformance-testing utilities (Match + Conformance runners) for @adobe/data ECS features",
"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.10.26",
"version": "0.10.27",
"description": "Adobe data oriented programming library",
"type": "module",
"sideEffects": false,
Expand Down
2 changes: 1 addition & 1 deletion packages/data/src/service/async-data-service/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ See [create-lazy.md](./create-lazy.md) for complete documentation.

### `AsyncDataService.toObserveState(service)`

Observes every `Observe` member a service's schema describes — recursing through nested organizational groups — in parallel, and emits one combined object with the service's nested shape, typed as `AsyncDataService.State<T>` (observe members become their values; actions and action-only groups are omitted). Nothing is emitted until every member has a value; the member set is re-derived when `service.schema` changes. The service must expose its `schema`.
Observes every `Observe` member a service's schema describes — recursing through nested organizational groups — in parallel, and emits one combined object with the service's nested shape, typed as `AsyncDataService.State<T>` (observe members become their values, `undefined` until resolved; actions and action-only groups are omitted). It emits immediately without waiting for every member, then at most once per microtask as members change; the member set is re-derived when `service.schema` changes. The service must expose its `schema`.

```typescript
const unobserve = AsyncDataService.toObserveState(service)((state) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// © 2026 Adobe. MIT License. See /LICENSE for details.

import { describe, expect, it } from "vitest";
import { Observe } from "../../observe/index.js";
import { fromPartialProperties } from "./from-partial-properties.js";

describe("fromPartialProperties", () => {
it("emits unresolved members as undefined, then batches changes per microtask", async () => {
const [a, setA] = Observe.createState(1);
const [b, setB] = Observe.createState<string>();
const values: unknown[] = [];
const unobserve = fromPartialProperties({ a, b })((value) => values.push(value));
expect(values).toEqual([{ a: 1, b: undefined }]);

setA(2);
setB("x");
expect(values).toHaveLength(1);
await Promise.resolve();
expect(values).toEqual([{ a: 1, b: undefined }, { a: 2, b: "x" }]);

setA(3);
unobserve();
await Promise.resolve();
expect(values).toHaveLength(2);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// © 2026 Adobe. MIT License. See /LICENSE for details.

import { Observe } from "../../observe/index.js";

/**
* Combines named observe functions into one whose value has the same keys, without
* waiting for all of them: a member that has not yet emitted reads as `undefined`
* (never a valid `Data` value, so it unambiguously means "not resolved yet").
*
* Emits synchronously once on subscribe with whatever resolved synchronously, then at
* most once per microtask however many members change within it.
*/
export function fromPartialProperties<T extends { readonly [K: string]: Observe<unknown> }>(
observeProperties: T,
): Observe<{ readonly [K in keyof T]: (T[K] extends Observe<infer U> ? U : never) | undefined }> {
type Values = { readonly [K in keyof T]: (T[K] extends Observe<infer U> ? U : never) | undefined };
return (observer) => {
const values: Record<string, unknown> = {};
for (const key of Object.keys(observeProperties)) values[key] = undefined;
let initializing = true;
let scheduled = false;
let active = true;
// Every key of `T` is assigned above, and each value is the latest from that key's observe.
const notify = () => observer({ ...values } as Values);
const unobservers = Object.entries(observeProperties).map(([key, observe]) =>
observe((value) => {
values[key] = value;
if (initializing || scheduled) return;
scheduled = true;
queueMicrotask(() => {
scheduled = false;
if (active) notify();
});
}),
);
initializing = false;
notify();
return () => {
active = false;
for (const unobserve of unobservers) unobserve();
};
};
}
11 changes: 6 additions & 5 deletions packages/data/src/service/async-data-service/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,19 @@
import { Observe } from "../../observe/index.js";
import { Service } from "../service.js";

// A member's state value: an `Observe<T>` becomes `T`, a nested organizational group
// becomes its own `State` (dropped when it holds no observe members), and anything
// else (an action) is `never`, which removes the key.
// A member's state value: an `Observe<T>` becomes `T | undefined` (undefined until it
// resolves), a nested organizational group becomes its own `State` (dropped when it
// holds no observe members), and anything else (an action) is `never`, which removes
// the key.
type StateValue<P> =
P extends Observe<infer T> ? T
P extends Observe<infer T> ? T | undefined
: P extends (...args: any[]) => any ? never
: P extends object ? (keyof State<P> extends never ? never : State<P>)
: never;

/**
* The current-value snapshot of a service: the same nested shape as `T`, keeping only
* its `Observe<X>` members (converted to `X`) and the groups that contain them. Actions
* its `Observe<X>` members (converted to `X | undefined`, undefined until resolved) and the groups that contain them. Actions
* and base-`Service` metadata are omitted. Produced at runtime by `toObserveState`.
*/
export type State<T> = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,13 @@ interface TestService extends Service {
readonly optional?: Observe<string>;
}

// Observe members become their values, actions and action-only groups are omitted,
// optional members stay optional.
// Observe members become their values (undefined until resolved), actions and
// action-only groups are omitted, optional members stay optional.
type _CheckState = Assert<EquivalentTypes<AsyncDataService.State<TestService>, {
readonly count: number;
readonly foo: { readonly state: { readonly a: string; readonly b: boolean } };
readonly "dotted.key": number;
readonly optional?: string;
readonly count: number | undefined;
readonly foo: { readonly state: { readonly a: string | undefined; readonly b: boolean | undefined } };
readonly "dotted.key": number | undefined;
readonly optional?: string | undefined;
}>>;

function createTestService() {
Expand Down Expand Up @@ -87,19 +87,21 @@ describe("AsyncDataService.toObserveState", () => {
unobserve();
});

it("re-emits when any member changes and stops after unobserve", () => {
it("re-emits once per microtask when members change and stops after unobserve", async () => {
const { service, setCount, setA } = createTestService();
const values: AsyncDataService.State<TestService>[] = [];
const unobserve = AsyncDataService.toObserveState(service)((value) => values.push(value));
setA("y");
setCount(2);
expect(values.map((value) => [value.count, value.foo.state.a])).toEqual([[1, "x"], [1, "y"], [2, "y"]]);
await Promise.resolve();
expect(values.map((value) => [value.count, value.foo.state.a])).toEqual([[1, "x"], [2, "y"]]);
unobserve();
setCount(3);
expect(values).toHaveLength(3);
await Promise.resolve();
expect(values).toHaveLength(2);
});

it("waits until every member has a value", () => {
it("emits without waiting for unresolved members, which read as undefined", async () => {
const [late, setLate] = Observe.createState<number>();
const service = {
schema: Observe.fromConstant<Schema>({
Expand All @@ -111,9 +113,10 @@ describe("AsyncDataService.toObserveState", () => {
};
const values: unknown[] = [];
AsyncDataService.toObserveState(service)((value) => values.push(value));
expect(values).toEqual([]);
expect(values).toEqual([{ now: 1, late: undefined }]);
setLate(2);
expect(values).toEqual([{ now: 1, late: 2 }]);
await Promise.resolve();
expect(values).toEqual([{ now: 1, late: undefined }, { now: 1, late: 2 }]);
});

it("skips members the schema describes but the instance lacks", () => {
Expand Down
10 changes: 6 additions & 4 deletions packages/data/src/service/async-data-service/to-observe-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@
import { Observe } from "../../observe/index.js";
import { Schema } from "../../schema/index.js";
import { Service } from "../service.js";
import { fromPartialProperties } from "./from-partial-properties.js";
import { State } from "./state.js";

/**
* Observes every `Observe` member of a service — found by recursively walking its
* schema, through nested organizational groups — in parallel, and emits them combined
* into one object with the service's nested shape (`foo.state.b` is read as
* `state.foo.state.b`). Like `Observe.fromProperties`, nothing is emitted until every
* member has a value, then once per change. When the service's schema changes, the
* member set is re-derived.
* `state.foo.state.b`). It does not wait for every member: one that has not resolved
* yet reads as `undefined`. It emits once on subscribe, then at most once per microtask
* however many members change. When the service's schema changes, the member set is
* re-derived.
*
* Every observe member the schema describes is included, regardless of its `external`
* policy; members the schema describes but the instance lacks (optional) are skipped.
Expand All @@ -24,7 +26,7 @@ function observeState<T extends Service>(service: T, schema: Schema): Observe<St
const paths: (readonly string[])[] = [];
const members: Record<string, Observe<unknown>> = {};
collect(service, schema, [], paths, members);
return Observe.withMap(Observe.fromProperties(members), (values) => {
return Observe.withMap(fromPartialProperties(members), (values) => {
const state: Record<string, unknown> = {};
paths.forEach((path, index) => {
let target = state;
Expand Down
Loading