@adobe/data-ai 0.9.86 → 0.9.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/rules/cohesion.md +2 -2
- package/.claude/rules/data-modelling.md +2 -0
- package/.claude/rules/ecs.md +275 -0
- package/.claude/rules/element.md +3 -1
- package/.claude/rules/features/ecs/systems.md +57 -0
- package/.claude/rules/features/ui/binding-element.md +226 -0
- package/.claude/rules/features/ui/lazy-wrapper.md +27 -0
- package/.claude/rules/features/ui/presentation-test.md +38 -0
- package/.claude/rules/{presentation.md → features/ui/presentation.md} +2 -32
- package/.claude/rules/function-references.md +2 -2
- package/.claude/rules/hooks.md +57 -0
- package/.claude/rules/layout.md +132 -0
- package/.claude/rules/lazy-element.md +1 -1
- package/.claude/rules/lit.md +115 -0
- package/.claude/rules/namespace.md +93 -135
- package/.claude/rules/observe.md +183 -0
- package/.claude/rules/react.md +102 -0
- package/.claude/rules/service.md +164 -0
- package/.claude/rules/type-casts.md +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +2 -2
- package/package.json +3 -3
- package/.claude/rules/archetypes.md +0 -60
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- '**/services/**/*.ts'
|
|
4
|
+
- '**/*-plugin.ts'
|
|
5
|
+
- '**/*-plugin/**/*.ts'
|
|
6
|
+
- '**/*dependent-state*.ts'
|
|
7
|
+
- '**/*dependent-state*/**/*.ts'
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Observe pattern
|
|
11
|
+
|
|
12
|
+
`Observe<T>` from `@adobe/data/observe` — a subscription function: `(notify: (value: T) => void) => Unobserve`. Callback may be invoked
|
|
13
|
+
synchronously or asynchronously, zero or more times. Returns `Unobserve` to stop observing.
|
|
14
|
+
|
|
15
|
+
**Always use `Observe.*` helpers to create and manipulate observables. Never write raw subscriptions or manual notify wrappers** — the
|
|
16
|
+
standard helpers cover every composition need and keep the code consistent.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Creation helpers
|
|
21
|
+
|
|
22
|
+
| Helper | Usage |
|
|
23
|
+
| ------------------------------------ | -------------------------------------------------- |
|
|
24
|
+
| `Observe.fromConstant(value)` | `Observe<T>` from a constant value |
|
|
25
|
+
| `Observe.fromProperties({ a, b })` | Combine named observables into `Observe<{ a, b }>` |
|
|
26
|
+
| `Observe.fromArray([obs1, obs2])` | Combine array of observables into `Observe<T[]>` |
|
|
27
|
+
| `Observe.fromPromise(() => Promise)` | Lazy; notifies once when promise resolves |
|
|
28
|
+
| `Observe.createState(initial?)` | `[Observe<T>, (value: T) => void]` — mutable state |
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
const constant = Observe.fromConstant(42);
|
|
32
|
+
const combined = Observe.fromProperties({ a: db.observe.resources.a, b: db.observe.resources.b });
|
|
33
|
+
const [count, setCount] = Observe.createState(0);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Transformation helpers
|
|
39
|
+
|
|
40
|
+
| Helper | Usage |
|
|
41
|
+
| ----------------------------------- | ------------------------------------------------------------ |
|
|
42
|
+
| `Observe.withMap(obs, fn)` | Transform value; map from one type to another |
|
|
43
|
+
| `Observe.withFilter(obs, fn)` | Transform or filter; return `undefined` to skip |
|
|
44
|
+
| `Observe.withDefault(default, obs)` | Use default when value is undefined |
|
|
45
|
+
| `Observe.withLazy(() => obs)` | Defer expensive observable creation until first subscription |
|
|
46
|
+
| `Observe.withUnwrap(obs)` | Flatten `Observe<Observe<T>>` to `Observe<T>` |
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
const doubled = Observe.withMap(count, n => n * 2);
|
|
50
|
+
const max = Observe.withFilter(Observe.fromProperties({ a, b }), ({ a, b }) => Math.max(a, b));
|
|
51
|
+
const withFallback = Observe.withDefault('unknown', maybeName);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Conversion helpers
|
|
57
|
+
|
|
58
|
+
| Helper | Usage |
|
|
59
|
+
| ------------------------ | ----------------------------------- |
|
|
60
|
+
| `Observe.toPromise(obs)` | Resolve with first value (one-shot) |
|
|
61
|
+
|
|
62
|
+
`Observe.toPromise` is for services reading a current value — **never use it in UI elements**.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## ⛔ Antipattern: synchronous "peek"
|
|
67
|
+
|
|
68
|
+
**Never read an observable's current value by subscribing and immediately unsubscribing.** The `notify` callback may fire asynchronously
|
|
69
|
+
(auth state, network, derived chains), so the snapshot is stale or `undefined`.
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
// WRONG — only works if the observable happens to emit synchronously on subscribe
|
|
73
|
+
let value: T | undefined;
|
|
74
|
+
const unobserve = someObservable(v => {
|
|
75
|
+
value = v;
|
|
76
|
+
});
|
|
77
|
+
unobserve();
|
|
78
|
+
// use `value` ← undefined for any async-resolving observable
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
// CORRECT — resolve the current value explicitly
|
|
83
|
+
const value = await Observe.toPromise(someObservable);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
A stored `unobserve` is **only** ever for deferred teardown (returned from an Effect, called on unmount/cleanup) — never to bookend a
|
|
87
|
+
synchronous read. If a sync call site (e.g. a DOM event handler that must call `preventDefault()` before any `await`) needs the value, do
|
|
88
|
+
the synchronous work first, then `void asyncHelper(...)` that awaits `Observe.toPromise`. In a Lit render path, read reactive values through
|
|
89
|
+
`useObservableValues`, not `toPromise`.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## ⛔ Antipattern: `undefined` as an Observe data value
|
|
94
|
+
|
|
95
|
+
`@adobe/data/observe` reserves `undefined` to mean **"no value has been emitted yet"** — it gates the synchronous initial emit in
|
|
96
|
+
`Observe.createState`, `fromProperties`, and friends. A data value flowing through an Observe pipeline must use `null` for the absent case,
|
|
97
|
+
**never** `undefined`.
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
// WRONG — Observe<X | undefined>
|
|
101
|
+
const [projectId, setProjectId] = Observe.createState<ProjectId | undefined>(undefined);
|
|
102
|
+
return Observe.withDefault(undefined, cloudSync.projectName);
|
|
103
|
+
|
|
104
|
+
// CORRECT — Observe<X | null>
|
|
105
|
+
const [projectId, setProjectId] = Observe.createState<ProjectId | null>(null);
|
|
106
|
+
return Observe.withDefault<string | null>(null, cloudSync.projectName);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Why it bites:
|
|
110
|
+
|
|
111
|
+
- `createState<T | undefined>(undefined)` does **not** fire on subscribe (the implementation treats `undefined` as "not yet set"), so
|
|
112
|
+
downstream `useObservableValues` callers wait forever for a value that never arrives.
|
|
113
|
+
- `fromProperties` re-emits when an input flips `undefined → defined`, not when a value changes — using `undefined` as data corrupts the
|
|
114
|
+
dirty-tracking.
|
|
115
|
+
- `null` JSON-stringifies across the wire; `undefined` silently disappears.
|
|
116
|
+
|
|
117
|
+
When auditing new code, flag `Observe<… | undefined>`, `createState<… | undefined>`, `withDefault(undefined`, and `fromConstant(undefined)`
|
|
118
|
+
— each should use `null`. (A plain function returning `undefined`, e.g. an early `return` from an async helper, is unrelated and fine.)
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## ⛔ Antipattern: membership-only subscription for value reads (ECS)
|
|
123
|
+
|
|
124
|
+
`db.observe.select(archetype.components)` fires **only when entities enter or leave the archetype** — i.e. on set-membership changes. It
|
|
125
|
+
does **not** fire when a component value changes on an entity already in the set. If the callback reads component **values**, this
|
|
126
|
+
subscription is almost always wrong: it emits once on load, then silently stops reacting when the user edits values (opacity slider,
|
|
127
|
+
selection toggle, drag end). The derived state goes stale with no error — the hardest bug to catch.
|
|
128
|
+
|
|
129
|
+
Pick the subscription by what the callback reads:
|
|
130
|
+
|
|
131
|
+
| Callback needs | Use |
|
|
132
|
+
| ----------------------------------------------------------------------- | --------------------------------------------------------- |
|
|
133
|
+
| Entity **IDs** only ("how many items," "does this track have children") | `db.observe.select(archetype.components)` |
|
|
134
|
+
| Component **values** (any `db.get(id, 'field')` in a map/filter) | `Database.observeSelectDeep(db, [...fields] as const)` |
|
|
135
|
+
| A single UI element bound to one entity | `db.observe.entity(id)` / `observe.entity(id, archetype)` |
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
// WRONG — reads values but only reacts to add/remove; value edits never re-run this
|
|
139
|
+
Observe.withMap(db.observe.select(archetype.components), ids => ids.filter(id => (db.get(id, 'selectedByUsers') ?? []).includes(userId)));
|
|
140
|
+
|
|
141
|
+
// CORRECT — value-tracking subscription; list exactly the fields read downstream
|
|
142
|
+
const SELECTED_FIELDS = ['selectedByUsers'] as const; // hoist the tuple above the factory
|
|
143
|
+
const rows$ = Database.observeSelectDeep(db, SELECTED_FIELDS);
|
|
144
|
+
Observe.withMap(rows$, rows => rows.filter(r => r.selectedByUsers?.includes(userId)).map(r => r.id));
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`observeSelectDeep` re-emits on **any** change to **any** listed component on **any** matched entity — so list only the fields actually
|
|
148
|
+
read, and when the matched set is large pair it with a structural deduplicate downstream so identical-looking emissions don't propagate
|
|
149
|
+
(`@adobe/data/observe`'s `withDeduplicate` is reference-equality only). `observeSelectDeep` from a UI element is banned — let each element
|
|
150
|
+
observe its own entity (see `.claude/rules/ecs.md` and `.claude/rules/squirrel/ui-data-access.md`).
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Common patterns
|
|
155
|
+
|
|
156
|
+
**Derived observable from service:**
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
export const isDevelopment = memoize((service: Pick<EnvironmentService, 'type'>) =>
|
|
160
|
+
Observe.withMap(service.type, type => type === 'development')
|
|
161
|
+
);
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Computed from multiple resources:**
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
Observe.withFilter(Observe.fromProperties({ a: db.observe.resources.a, b: db.observe.resources.b }), ({ a, b }) => Math.max(a, b));
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**Lazy creation:** `Observe.withLazy(() => expensiveObs)` — defers until first subscription.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Execute
|
|
175
|
+
|
|
176
|
+
When adding or using Observe:
|
|
177
|
+
|
|
178
|
+
1. Use `Observe.fromConstant` for static values.
|
|
179
|
+
2. Use `Observe.fromProperties` to combine multiple observables.
|
|
180
|
+
3. Use `Observe.withFilter` when mapping and/or filtering.
|
|
181
|
+
4. Use `Observe.createState` for mutable state with a setter.
|
|
182
|
+
5. Use `Observe.withUnwrap` to flatten `Observe<Observe<T>>`.
|
|
183
|
+
6. Call `unobserve()` when cleaning up (e.g. component unmount).
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- '**/*.tsx'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# React component authoring
|
|
7
|
+
|
|
8
|
+
React components live in the `components/` layer. Consume Observe and void actions from plugins. Use `@adobe/data-react` for `useDatabase`,
|
|
9
|
+
`useObservableValues`, and `DatabaseProvider`.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## useDatabase — single context
|
|
14
|
+
|
|
15
|
+
Binding components **must** call `useDatabase` to obtain the main service context. Do **not** access any other React context. Additional
|
|
16
|
+
contexts create a context waterfall and hurt performance. All other services and state are reachable from the database.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Binding component vs presentation
|
|
21
|
+
|
|
22
|
+
**Binding component** — the React component that:
|
|
23
|
+
|
|
24
|
+
- Injects observed values via `useObservableValues`
|
|
25
|
+
- Triggers re-render when those values change
|
|
26
|
+
- Binds action callbacks to the presentation
|
|
27
|
+
|
|
28
|
+
**Presentation** — a pure function (no hooks) that receives data and action callbacks as props and returns JSX. Keep reactive logic in the
|
|
29
|
+
binding component; presentation stays pure.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Props from parent
|
|
34
|
+
|
|
35
|
+
**Do not** pass values from parent except when needed to identify which entity in the database to bind to.
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
// Parent: passes entity so child knows which record to bind to
|
|
39
|
+
{
|
|
40
|
+
values.sprites.map(entity => <Sprite key={entity} entity={entity} />);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Child: uses entity prop to observe the right record
|
|
44
|
+
function Sprite({ entity }: { entity: Entity }) {
|
|
45
|
+
const db = useDatabase();
|
|
46
|
+
const values = useObservableValues(() => ({ sprite: db.observe.entity(entity, db.archetypes.Sprite) }), [entity]);
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## useObservableValues
|
|
53
|
+
|
|
54
|
+
_Most_ binding components use a **single** `useObservableValues` call. Collect all observed values in one object. Use `Observe.withDefault`
|
|
55
|
+
for values that may resolve slowly.
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
function Counter() {
|
|
59
|
+
const db = useDatabase(counterPlugin);
|
|
60
|
+
const values = useObservableValues(() => ({
|
|
61
|
+
count: db.observe.resources.count,
|
|
62
|
+
}));
|
|
63
|
+
if (!values) return null;
|
|
64
|
+
return presentation.render({ ...values, increment: db.transactions.increment });
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Presentation exports
|
|
71
|
+
|
|
72
|
+
Presentation files ONLY export `render` (and localization bundles where appropriate). Nothing else. If you need render args type externally,
|
|
73
|
+
use `Parameters<typeof render>[0]`.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Action callbacks (not events)
|
|
78
|
+
|
|
79
|
+
Use **verb** or **verbNoun** names — never `on*` prefix. See `.claude/rules/presentation.md` for full examples.
|
|
80
|
+
|
|
81
|
+
**Pass function references directly** when the signature matches; wrap in an arrow function only to supply arguments:
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
increment: db.transactions.increment; // reference — no wrapper needed
|
|
85
|
+
toggleSprite: () => db.transactions.toggleSpriteActive({ entity }); // args required — wrap
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Execute
|
|
91
|
+
|
|
92
|
+
When creating or modifying a React component:
|
|
93
|
+
|
|
94
|
+
1. Call `useDatabase` for the main service context. Do not use any other React context.
|
|
95
|
+
2. Split into binding component (reactive) and presentation (pure).
|
|
96
|
+
3. Use a single `useObservableValues` in the binding component.
|
|
97
|
+
4. Pass observed values and action callbacks to presentation. Pass function references directly when the signature matches; wrap only when
|
|
98
|
+
supplying arguments.
|
|
99
|
+
5. Keep presentation pure — no hooks.
|
|
100
|
+
6. Presentation exports only `render` (and localization bundles where appropriate).
|
|
101
|
+
7. Add `*-presentation.test.tsx` for presentation when appropriate; do not unit test binding components.
|
|
102
|
+
8. Never include business logic within binding components. Move into computed values or action handlers.
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- '**/services/**/*.ts'
|
|
4
|
+
- '**/*-service.ts'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Service authoring
|
|
8
|
+
|
|
9
|
+
Asynchronous data services. Live in the `services/` layer. Adhere to the namespace rule for type and function organization.
|
|
10
|
+
|
|
11
|
+
**Data** = readonly JSON values or Blobs.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Front-end vs back-end services
|
|
16
|
+
|
|
17
|
+
**Front-end services** — called directly from UI components. Support unidirectional control flow.
|
|
18
|
+
|
|
19
|
+
| Allowed | Not allowed |
|
|
20
|
+
| ------------------------------- | -------------------------------- |
|
|
21
|
+
| `Observe<Data>` | Promise or AsyncGenerator return |
|
|
22
|
+
| Other services (sub-Services) | |
|
|
23
|
+
| Void-returning action functions | |
|
|
24
|
+
|
|
25
|
+
**Back-end services** — usually stateless. Functions typically return `Promise<Data>` or `AsyncGenerator<Data>`. Not called directly from
|
|
26
|
+
UI; front-end services call them.
|
|
27
|
+
|
|
28
|
+
This separation keeps UI components on a strict unidirectional path: data down via void actions, data up via Observe.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Front-end service constraints
|
|
33
|
+
|
|
34
|
+
`Service` interfaces for UI consumption may only contain:
|
|
35
|
+
|
|
36
|
+
- **`Observe<Data>`** — observable properties or factories
|
|
37
|
+
- **Sub-Service**s — nested service interfaces
|
|
38
|
+
- **Action functions** — zero or more `Data` arguments, return `void` only
|
|
39
|
+
- **Factory functions** — create observables or sub-Services
|
|
40
|
+
|
|
41
|
+
Compile-time check: `Assert<AsyncDataService.IsValid<ServiceInterface>>`.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Folder structure and cohesion
|
|
46
|
+
|
|
47
|
+
**MUST NOT** place implementation files alongside the interface file. The `<name>-service/` folder MUST contain only the interface file and
|
|
48
|
+
`public.ts`.
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
services/<name>-service/
|
|
52
|
+
<name>-service.ts ← interface + contract types; `export * as ServiceName from './public.js'` at bottom
|
|
53
|
+
public.ts ← re-exports factory (and other public exports) from the implementation sub-folder
|
|
54
|
+
<impl-name>/ ← REQUIRED sub-folder for all implementation files
|
|
55
|
+
create-<name>-service.ts ← factory function
|
|
56
|
+
*.ts ← helpers, utils, constants (implementation-private)
|
|
57
|
+
*.test.ts ← tests live here alongside implementation
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Single-file exception**: a bare factory with no helpers, no utilities, and no constants MAY live as a flat file alongside the interface.
|
|
61
|
+
The moment any additional implementation file is needed, the factory MUST move into a sub-folder.
|
|
62
|
+
|
|
63
|
+
**Interface file** (`<name>-service.ts`) — interface declaration and types that define the public service contract. MUST NOT import from any
|
|
64
|
+
implementation file or sub-folder. At the bottom of the file, exports the namespace: `export * as ServiceName from './public.js'`. This
|
|
65
|
+
merges the interface type and the factory namespace under one identifier — callers type against the interface and invoke factory functions
|
|
66
|
+
through the same name.
|
|
67
|
+
|
|
68
|
+
**Separate types file at the interface level** — only permitted if it contains exclusively public-contract types (types that appear in the
|
|
69
|
+
interface's method signatures or `Observe<>` generic arguments). Implementation-detail types — third-party API shapes, `window.*` global
|
|
70
|
+
augmentations, internal utilities — MUST live inside the implementation sub-folder.
|
|
71
|
+
|
|
72
|
+
**`*.test.ts` files MUST NOT appear at the interface folder level.** Every test file belongs inside the implementation sub-folder next to
|
|
73
|
+
the code it tests; there is nothing at the interface level to test.
|
|
74
|
+
|
|
75
|
+
**Implementation sub-folder** — all implementation logic including the factory, utilities, constants, helpers, and their tests. MUST NOT
|
|
76
|
+
depend on the interface file's types via the `public.js` re-export; import directly from `<name>-service.js`.
|
|
77
|
+
|
|
78
|
+
**public.ts** — MUST re-export the factory function and any other public exports from the implementation sub-folder. Does NOT re-export the
|
|
79
|
+
interface — the interface is exported directly from `<name>-service.ts`.
|
|
80
|
+
|
|
81
|
+
**No classes** — factory function or static plain object only. `this` bindings are brittle and block higher-order composition.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Factory export shape — `singletonFactory` + dual export
|
|
86
|
+
|
|
87
|
+
Every service factory MUST be wrapped with `singletonFactory` from `factory-functions.js`. This guarantees one instance per argument tuple
|
|
88
|
+
when the same factory is called multiple times, preventing duplicate subscriptions and torn state.
|
|
89
|
+
|
|
90
|
+
Export **both** the raw `factory` and the singleton-wrapped `create`:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
// <name>-service/<impl-name>/create-<name>-service.ts
|
|
94
|
+
import { singletonFactory } from 'factory-functions.js';
|
|
95
|
+
|
|
96
|
+
export const factory = (deps: Deps): NameService => {
|
|
97
|
+
// implementation
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// `factory` — for test injection (bypasses the singleton cache; each test gets a fresh instance)
|
|
101
|
+
// `create` — for production use (singleton per deps-tuple)
|
|
102
|
+
export const create = singletonFactory(factory);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`public.ts` re-exports both:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
export { create, factory } from './<impl-name>/create-<name>-service.js';
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Why:** Tests must create fresh instances without sharing a singleton cache. Production callers use `create`; tests that need isolation
|
|
112
|
+
import `factory` directly.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Service interface cleanliness
|
|
117
|
+
|
|
118
|
+
The `<name>-service.ts` interface MUST NOT reference internal implementation tools, third-party SDK names, or infrastructure system
|
|
119
|
+
identifiers in any method signature, property name, or type parameter.
|
|
120
|
+
|
|
121
|
+
**Wrong** — leaks an internal infrastructure name into the UI contract:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
interface SessionService extends Service {
|
|
125
|
+
readonly dunamis: DunamisSdk; // UI now knows Dunamis exists
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Correct** — surfaces only what UI needs, implementation-tool-agnostic:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
interface SessionService extends Service {
|
|
133
|
+
readonly sessionId: Observe<string>;
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The interface is the contract between UI and data layers. If the underlying tool is swapped (Dunamis → another SDK, cookies → localStorage),
|
|
138
|
+
the interface must not change. Any leak of an internal tool name into the interface couples the UI to an implementation detail it must not
|
|
139
|
+
know about.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Execute
|
|
144
|
+
|
|
145
|
+
When creating or modifying a service:
|
|
146
|
+
|
|
147
|
+
1. Place in `services/<name>-service/`.
|
|
148
|
+
2. Interface in `<name>-service.ts` — types only, extend `Service`, add `Assert<AsyncDataService.IsValid<>>`.
|
|
149
|
+
3. All implementation files go inside a named sub-folder (e.g. `create-<name>-service/`).
|
|
150
|
+
4. At the bottom of `<name>-service.ts`, add `export * as ServiceName from './public.js'`.
|
|
151
|
+
5. In `public.ts`, re-export the factory (and any other public exports) from the implementation sub-folder.
|
|
152
|
+
6. For front-end services: actions return void only; observables observe Data or Service only. For back-end services: functions return
|
|
153
|
+
`Promise<Data>` or `AsyncGenerator<Data>`.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Verify
|
|
158
|
+
|
|
159
|
+
When any `src/**/services/**/*.ts` file is in the diff, confirm all three invariants:
|
|
160
|
+
|
|
161
|
+
1. **Interface/implementation separation** — every `.ts` file added or modified in `<name>-service/` is either `<name>-service.ts`,
|
|
162
|
+
`public.ts`, or inside a named sub-folder. Any file at the root of the folder that is neither is a violation.
|
|
163
|
+
2. **`public.ts` completeness** — `public.ts` re-exports the factory function (and any other public exports from the sub-folder).
|
|
164
|
+
3. **Interface file purity** — `<name>-service.ts` contains no imports from implementation sub-folders.
|
package/README.md
CHANGED
|
@@ -76,5 +76,5 @@ This presumes you are already running the agent within the directory you want to
|
|
|
76
76
|
|
|
77
77
|
```
|
|
78
78
|
Find the @adobe/data-ai package on npmjs and then install it in the current directory.
|
|
79
|
-
Next use it's /build-application skill to build <
|
|
80
|
-
```
|
|
79
|
+
Next use it's /build-application skill to build <prompt here>
|
|
80
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe/data-ai",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.87",
|
|
4
4
|
"description": "Cross-agent architecture skills for @adobe/data — installable as a Claude Code plugin or copied into any Agent-Skills-compatible agent (Cursor, Codex).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -29,12 +29,12 @@
|
|
|
29
29
|
"skills"
|
|
30
30
|
],
|
|
31
31
|
"peerDependencies": {
|
|
32
|
-
"@adobe/data": "0.9.
|
|
32
|
+
"@adobe/data": "0.9.87"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"typescript": "^5.8.3",
|
|
36
36
|
"vitest": "^1.6.0",
|
|
37
|
-
"@adobe/data": "0.9.
|
|
37
|
+
"@adobe/data": "0.9.87"
|
|
38
38
|
},
|
|
39
39
|
"license": "MIT",
|
|
40
40
|
"scripts": {
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Authoring rules for ECS systems that iterate archetype rows.
|
|
3
|
-
globs: "**/*.ts"
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Archetypes — iteration rules
|
|
7
|
-
|
|
8
|
-
## Express selection in the query, not the loop
|
|
9
|
-
|
|
10
|
-
`queryArchetypes(include, { exclude })` accepts both required and excluded
|
|
11
|
-
component lists. Use them. Don't query a wider set and then skip rows or
|
|
12
|
-
archetypes with an `if`.
|
|
13
|
-
|
|
14
|
-
```ts
|
|
15
|
-
// ❌ post-filter
|
|
16
|
-
for (const arch of db.store.queryArchetypes(["position", "rotation"])) {
|
|
17
|
-
if (arch.components.has("_worldMatrix")) continue;
|
|
18
|
-
...
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// ✅ declarative
|
|
22
|
-
for (const arch of db.store.queryArchetypes(
|
|
23
|
-
["position", "rotation"],
|
|
24
|
-
{ exclude: ["_worldMatrix"] },
|
|
25
|
-
)) {
|
|
26
|
-
...
|
|
27
|
-
}
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
## When every row migrates out, iterate tail → head
|
|
31
|
-
|
|
32
|
-
Archetypes are densely packed. Removing or migrating a row that isn't the
|
|
33
|
-
last one triggers a hole-fill: the tail row is moved into the gap.
|
|
34
|
-
Iterating `0 → rowCount-1` while migrating every row pays this cost on
|
|
35
|
-
every iteration. Iterating `rowCount-1 → 0` means each removal is from
|
|
36
|
-
the tail — no shift, indices ahead of the cursor stay valid.
|
|
37
|
-
|
|
38
|
-
```ts
|
|
39
|
-
// ❌ shifts the tail into every hole, and the snapshot allocation is
|
|
40
|
-
// only there to survive the shifts.
|
|
41
|
-
const ids = [...];
|
|
42
|
-
for (let i = 0; i < arch.rowCount; i++) ids[i] = arch.columns.id.get(i);
|
|
43
|
-
for (const id of ids) db.store.update(id, { _worldMatrix: Mat4x4.identity });
|
|
44
|
-
|
|
45
|
-
// ✅ no shifts, no allocation
|
|
46
|
-
for (let i = arch.rowCount - 1; i >= 0; i--) {
|
|
47
|
-
db.store.update(arch.columns.id.get(i), { _worldMatrix: Mat4x4.identity });
|
|
48
|
-
}
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
If only *some* rows migrate (filter inside the loop), snapshot the ids
|
|
52
|
-
you'll touch — forward iteration is fine because rows you don't touch
|
|
53
|
-
stay put.
|
|
54
|
-
|
|
55
|
-
## Don't snapshot what the query already filters
|
|
56
|
-
|
|
57
|
-
A snapshot of "all entity ids in this archetype right now" is only
|
|
58
|
-
needed when forward iteration would invalidate. Reverse iteration
|
|
59
|
-
removes the need; an `exclude` clause removes the need to look at rows
|
|
60
|
-
that don't qualify in the first place.
|