diff --git a/package.json b/package.json index b1e7d500..b62b2ad7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "data-monorepo", - "version": "0.9.69", + "version": "0.9.70", "private": true, "scripts": { "build": "pnpm -r run build", diff --git a/packages/data-gpu-samples/package.json b/packages/data-gpu-samples/package.json index 05a1019e..58f9c704 100644 --- a/packages/data-gpu-samples/package.json +++ b/packages/data-gpu-samples/package.json @@ -1,6 +1,6 @@ { "name": "data-gpu-samples", - "version": "0.9.69", + "version": "0.9.70", "description": "WebGPU samples built on @adobe/data-gpu", "type": "module", "private": true, diff --git a/packages/data-gpu/package.json b/packages/data-gpu/package.json index ff157484..2dcd0eaf 100644 --- a/packages/data-gpu/package.json +++ b/packages/data-gpu/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-gpu", - "version": "0.9.69", + "version": "0.9.70", "description": "Adobe data WebGPU plugins and types for graphics and compute", "type": "module", "private": false, diff --git a/packages/data-lit-tictactoe/package.json b/packages/data-lit-tictactoe/package.json index 51bb046b..56dbaf0f 100644 --- a/packages/data-lit-tictactoe/package.json +++ b/packages/data-lit-tictactoe/package.json @@ -1,6 +1,6 @@ { "name": "data-lit-tictactoe", - "version": "0.9.69", + "version": "0.9.70", "description": "Tic-Tac-Toe sample - Lit web components with @adobe/data-lit and AgenticService", "type": "module", "private": true, diff --git a/packages/data-lit-todo/package.json b/packages/data-lit-todo/package.json index 4ed39ecf..1ea781f0 100644 --- a/packages/data-lit-todo/package.json +++ b/packages/data-lit-todo/package.json @@ -1,6 +1,6 @@ { "name": "data-lit-todo", - "version": "0.9.69", + "version": "0.9.70", "description": "Todo sample app demonstrating @adobe/data with Lit", "type": "module", "private": true, diff --git a/packages/data-lit/package.json b/packages/data-lit/package.json index 4c8b9a39..e23bd1d3 100644 --- a/packages/data-lit/package.json +++ b/packages/data-lit/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-lit", - "version": "0.9.69", + "version": "0.9.70", "description": "Adobe data Lit bindings - hooks, elements, decorators", "type": "module", "private": false, diff --git a/packages/data-p2p-tictactoe/package.json b/packages/data-p2p-tictactoe/package.json index c0c890f8..a8f2d59e 100644 --- a/packages/data-p2p-tictactoe/package.json +++ b/packages/data-p2p-tictactoe/package.json @@ -1,6 +1,6 @@ { "name": "data-p2p-tictactoe", - "version": "0.9.69", + "version": "0.9.70", "description": "Serverless P2P tic-tac-toe — WebRTC DataChannel + @adobe/data-sync", "type": "module", "private": true, diff --git a/packages/data-persistence/package.json b/packages/data-persistence/package.json index aa859ccf..443cd8ea 100644 --- a/packages/data-persistence/package.json +++ b/packages/data-persistence/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-persistence", - "version": "0.9.69", + "version": "0.9.70", "description": "Worker-based incremental persistence layer for @adobe/data ECS over OPFS (browser) and node:fs (server).", "type": "module", "sideEffects": false, diff --git a/packages/data-react-hello/package.json b/packages/data-react-hello/package.json index 8da0b0fa..2de3676c 100644 --- a/packages/data-react-hello/package.json +++ b/packages/data-react-hello/package.json @@ -1,6 +1,6 @@ { "name": "data-react-hello", - "version": "0.9.69", + "version": "0.9.70", "description": "Hello World sample - click counter using @adobe/data-react", "type": "module", "private": true, diff --git a/packages/data-react-pixie/package.json b/packages/data-react-pixie/package.json index cef08f53..29cbe8b7 100644 --- a/packages/data-react-pixie/package.json +++ b/packages/data-react-pixie/package.json @@ -1,6 +1,6 @@ { "name": "data-react-pixie", - "version": "0.9.69", + "version": "0.9.70", "description": "PixiJS React sample - ECS sprites (bunny, fox) with @adobe/data-react", "type": "module", "private": true, diff --git a/packages/data-react/package.json b/packages/data-react/package.json index 181e4872..eabca7e3 100644 --- a/packages/data-react/package.json +++ b/packages/data-react/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-react", - "version": "0.9.69", + "version": "0.9.70", "description": "Adobe data React bindings — hooks and context for ECS database", "type": "module", "private": false, diff --git a/packages/data-solid-dashboard/package.json b/packages/data-solid-dashboard/package.json index 2d294252..378c7b1a 100644 --- a/packages/data-solid-dashboard/package.json +++ b/packages/data-solid-dashboard/package.json @@ -1,6 +1,6 @@ { "name": "data-solid-dashboard", - "version": "0.9.69", + "version": "0.9.70", "description": "Mini dashboard sample — multiple components sharing one @adobe/data ECS database with SolidJS", "type": "module", "private": true, diff --git a/packages/data-solid/package.json b/packages/data-solid/package.json index 5298254f..6247aa37 100644 --- a/packages/data-solid/package.json +++ b/packages/data-solid/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-solid", - "version": "0.9.69", + "version": "0.9.70", "description": "Adobe data SolidJS bindings — context and provider for ECS database", "type": "module", "private": false, diff --git a/packages/data-sync/package.json b/packages/data-sync/package.json index 743e0d50..cec0f674 100644 --- a/packages/data-sync/package.json +++ b/packages/data-sync/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-sync", - "version": "0.9.69", + "version": "0.9.70", "description": "Multi-user real-time synchronisation for @adobe/data ECS — server, client, and in-process loopback.", "type": "module", "sideEffects": false, diff --git a/packages/data/package.json b/packages/data/package.json index e3b093c4..9e8f968c 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data", - "version": "0.9.69", + "version": "0.9.70", "description": "Adobe data oriented programming library", "type": "module", "sideEffects": false, diff --git a/packages/data/src/observe/with-batch.test.ts b/packages/data/src/observe/with-batch.test.ts index 5e253448..408ef6e0 100644 --- a/packages/data/src/observe/with-batch.test.ts +++ b/packages/data/src/observe/with-batch.test.ts @@ -1,5 +1,5 @@ // © 2026 Adobe. MIT License. See /LICENSE for details. -import { describe, it, expect, vi } from 'vitest'; +import { describe, it, expect } from 'vitest'; import { withBatch } from './with-batch.js'; import { createState } from './create-state.js'; @@ -118,4 +118,22 @@ describe('withBatch', () => { unsubscribe1(); unsubscribe2(); }); + + it('should not drop a batched undefined value', async () => { + const [source, setSource] = createState(1); + const batched = withBatch(source); + + const values: (number | undefined)[] = []; + const unsubscribe = batched((value) => values.push(value)); + + expect(values).toEqual([1]); + + setSource(undefined); + + await new Promise(resolve => setTimeout(resolve, 0)); + + expect(values).toEqual([1, undefined]); + + unsubscribe(); + }); }); \ No newline at end of file diff --git a/packages/data/src/observe/with-batch.ts b/packages/data/src/observe/with-batch.ts index 512fd46c..45f12498 100644 --- a/packages/data/src/observe/with-batch.ts +++ b/packages/data/src/observe/with-batch.ts @@ -8,7 +8,8 @@ import { Observe } from "./index.js"; */ export function withBatch(observable: Observe): Observe { return (observer) => { - let pendingValue: T | undefined; + let pendingValue: T; + let hasPendingValue = false; let isScheduled = false; let hasInitialValue = false; @@ -16,9 +17,10 @@ export function withBatch(observable: Observe): Observe { if (!isScheduled) { isScheduled = true; queueMicrotask(() => { - if (pendingValue !== undefined) { - observer(pendingValue); - pendingValue = undefined; + if (hasPendingValue) { + const value = pendingValue; + hasPendingValue = false; + observer(value); } isScheduled = false; }); @@ -27,19 +29,18 @@ export function withBatch(observable: Observe): Observe { const unobserve = observable((value) => { if (!hasInitialValue) { - // Emit initial value immediately observer(value); hasInitialValue = true; } else { - // Batch subsequent values pendingValue = value; + hasPendingValue = true; scheduleNotification(); } }); return () => { unobserve(); - pendingValue = undefined; + hasPendingValue = false; isScheduled = false; hasInitialValue = false; }; diff --git a/packages/data/src/observe/with-deduplicate-data.test.ts b/packages/data/src/observe/with-deduplicate-data.test.ts new file mode 100644 index 00000000..dfa4fe66 --- /dev/null +++ b/packages/data/src/observe/with-deduplicate-data.test.ts @@ -0,0 +1,43 @@ +// © 2026 Adobe. MIT License. See /LICENSE for details. +import { describe, it, expect } from 'vitest'; +import { withDeduplicateData } from './with-deduplicate-data.js'; +import { Observe } from './index.js'; + +describe('withDeduplicateData', () => { + it('should emit first value', () => { + const source: Observe = (observer) => { + observer(1); + return () => {}; + }; + const values: number[] = []; + withDeduplicateData(source)((v) => values.push(v)); + expect(values).toEqual([1]); + }); + + it('should deduplicate consecutive equal values', () => { + const source: Observe = (observer) => { + observer(1); + observer(1); + observer(2); + observer(2); + return () => {}; + }; + const values: number[] = []; + withDeduplicateData(source)((v) => values.push(v)); + expect(values).toEqual([1, 2]); + }); + + // Defensive: T extends Data excludes undefined at compile time, but runtime + // guards should hold even if undefined arrives via any/generics. + it('should deduplicate consecutive undefined values (defensive)', () => { + const source = ((observer: (v: null) => void) => { + (observer as (v: unknown) => void)(undefined); + (observer as (v: unknown) => void)(undefined); + return () => {}; + }) satisfies Observe; + + const values: (null | undefined)[] = []; + withDeduplicateData(source)((v: any) => values.push(v)); + expect(values).toEqual([undefined]); + }); +}); diff --git a/packages/data/src/observe/with-deduplicate-data.ts b/packages/data/src/observe/with-deduplicate-data.ts index f360c0ca..2449daf3 100644 --- a/packages/data/src/observe/with-deduplicate-data.ts +++ b/packages/data/src/observe/with-deduplicate-data.ts @@ -11,10 +11,12 @@ export function withDeduplicateData( observable: Observe ): Observe { return (observer) => { - let lastValue: T | undefined = undefined; + let notified = false; + let lastValue: T; return observable((value) => { - const notify = lastValue === undefined || !equals(lastValue, value); + const notify = !notified || !equals(lastValue, value); if (notify) { + notified = true; lastValue = value; observer(value); } diff --git a/packages/data/src/observe/with-filter.ts b/packages/data/src/observe/with-filter.ts index 23747dbf..1e237619 100644 --- a/packages/data/src/observe/with-filter.ts +++ b/packages/data/src/observe/with-filter.ts @@ -2,10 +2,11 @@ import { Observe } from "./index.js"; /** - * Creates a new Observe function that converts the original observe functions notify values into a new value at each notification, - * optionally returning undefined to filter out the value. + * Creates a new Observe function that converts values, using `undefined` (or `void`) as the skip signal. + * U must not include `undefined` — if the filter can legitimately produce `undefined` as a value, + * use `withMap` instead and handle absent values downstream. */ -export function withFilter( +export function withFilter( observable: Observe, filter: (value: T) => U | undefined | void ): Observe { diff --git a/packages/data/src/observe/with-filter.type-test.ts b/packages/data/src/observe/with-filter.type-test.ts new file mode 100644 index 00000000..372c1c44 --- /dev/null +++ b/packages/data/src/observe/with-filter.type-test.ts @@ -0,0 +1,16 @@ +// © 2026 Adobe. MIT License. See /LICENSE for details. + +import { fromConstant } from "./from-constant.js"; +import { withFilter } from "./with-filter.js"; + +// Valid uses: U is non-undefined +function _testValidUses() { + withFilter(fromConstant(1), (v) => v > 0 ? v : null); + withFilter(fromConstant("a"), (v) => v.length > 0 ? v : undefined); + withFilter(fromConstant(1), (v) => v > 0 ? v : undefined); +} + +// Invalid: undefined is the skip sentinel; U must not include undefined. +// Explicit annotation with U = number | undefined must be rejected. +// @ts-expect-error — U explicitly includes undefined, which is disallowed +withFilter(fromConstant(1), (v) => v > 0 ? v : undefined);