@prometheus-ags/entity-graph-core 3.0.4 → 3.1.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # @prometheus-ags/entity-graph-core
2
2
 
3
+ ## 3.1.0
4
+
5
+ ## 3.0.5
6
+
7
+ ### Patch Changes
8
+
9
+ - Resolve imperative graph access against the active graph instead of the package
10
+ singleton, so `GraphStoreProvider` scopes `useGraphStore.getState()`,
11
+ `.setState()`, `.subscribe()` and `.getInitialState()` (issue #42).
12
+
13
+ 3.0.4 replaced the copied StoreApi with delegates that warned but still targeted
14
+ the singleton, which left consumers using the documented imperative API with no
15
+ isolation. The React binding now proxies those methods, resolving per call.
16
+
17
+ New in `entity-graph-core`:
18
+ - `runWithGraphStore(store, fn)` — an `AsyncLocalStorage` request scope, so
19
+ concurrent server renders never share entity state.
20
+ - `prepareGraphStoreScope()` — awaited once during startup on **pure-ESM**
21
+ servers, where there is no synchronous `require` to load `node:async_hooks`.
22
+ Without it request scoping degrades to a module-level store and warns.
23
+ - `setActiveGraphStore(store)` — the module-level active store, set by
24
+ `GraphStoreProvider` on mount and restored on unmount.
25
+ - `resolveActiveGraphStore(fallback)` — request scope → module-level store →
26
+ fallback.
27
+
28
+ `node:async_hooks` is loaded lazily and never statically imported, so browser
29
+ bundles are unaffected. With no provider and no request scope, behaviour is
30
+ unchanged from 3.0.4, and the deprecation warning is gone because the methods
31
+ now do what callers expected.
32
+
3
33
  ## 3.0.4
4
34
 
5
35
  ## 3.0.3
package/README.md CHANGED
@@ -98,6 +98,252 @@ registerMergeStrategy("Document", strategy);
98
98
  The public `LoroModuleLoader` type describes this callback. Supplying it does
99
99
  not make Loro part of the mandatory core bundle.
100
100
 
101
+ ## Optional DevTools controller
102
+
103
+ The versioned DevTools runtime is isolated behind the
104
+ `@prometheus-ags/entity-graph-core/devtools` export. Normal application imports
105
+ do not expose the controller API.
106
+
107
+ ```ts
108
+ import { createGraphStore } from "@prometheus-ags/entity-graph-core";
109
+ import {
110
+ attachGraphDevtools,
111
+ createGraphDevtoolsClient,
112
+ } from "@prometheus-ags/entity-graph-core/devtools";
113
+
114
+ const store = createGraphStore();
115
+ const attachment = attachGraphDevtools(store, {
116
+ storeId: "admin-graph",
117
+ historyLimit: 500,
118
+ });
119
+
120
+ if (attachment.controller) {
121
+ const client = createGraphDevtoolsClient(
122
+ attachment.controller.storeId,
123
+ attachment.controller.connect("local-inspector"),
124
+ );
125
+
126
+ const snapshot = await client.request("get-snapshot");
127
+ client.disconnect();
128
+ }
129
+
130
+ attachment.detach();
131
+ ```
132
+
133
+ ### Inspect entities, rendered views, and relationships
134
+
135
+ The controller projects inspection state on demand from the owning graph. It
136
+ does not copy entity values into a second store:
137
+
138
+ ```ts
139
+ const attachment = attachGraphDevtools(store, {
140
+ storeId: "admin-graph",
141
+ values: { mode: "include" },
142
+ });
143
+
144
+ const controller = attachment.controller;
145
+ if (controller) {
146
+ const view = controller.registerView({
147
+ viewId: "users:active-table",
148
+ label: "Active users",
149
+ kind: "list",
150
+ entityType: "users",
151
+ queryKey: "users:active",
152
+ });
153
+
154
+ // Call this whenever the rendered membership changes.
155
+ view.updateMembership(["user-1", "user-2"]);
156
+
157
+ const entities = controller.getEntityRecords();
158
+ const views = controller.getViews();
159
+ const relationships = controller.getRelationships();
160
+
161
+ view.unregister();
162
+ }
163
+ ```
164
+
165
+ Entity records keep the canonical server-confirmed original, local graph patch,
166
+ and merged live value distinct. `dirtyReasons` names patched fields and
167
+ unsynchronized state; fetch/sync timestamps, typed fetch errors, store-local
168
+ revisions, and the stable IDs of views displaying the entity are projected
169
+ beside them. Missing canonical rows retained only by patch or metadata state are
170
+ reported as `presence: "missing-canonical"`.
171
+
172
+ View registrations are token-scoped. Duplicate registrations for one stable
173
+ `viewId` share the projected view but retain independent lifetimes, so one
174
+ consumer cannot unregister another. List views include current graph/list
175
+ statistics, and membership is available in both directions.
176
+
177
+ Relationships are derived from the existing CRUD schema registry and current
178
+ merged graph values. `belongsTo`, reverse `hasMany`, and `manyToMany` edges are
179
+ reported as `resolved` or `missing-target`; DevTools does not create a parallel
180
+ relationship registry.
181
+
182
+ ### Preview and restore a local patch
183
+
184
+ Preview commands write through the graph's existing patch layer, so every live
185
+ consumer observes the proposed value:
186
+
187
+ ```ts
188
+ const preview = await client.request("preview-entity-patch", {
189
+ type: "users",
190
+ id: "user-1",
191
+ patch: { displayName: "Proposed name" },
192
+ });
193
+
194
+ if (preview.ok && "previewId" in preview.result) {
195
+ const restore = await client.request("restore-entity-preview", {
196
+ previewId: preview.result.previewId,
197
+ });
198
+ }
199
+ ```
200
+
201
+ The receipt captures the exact prior patch. Restore replaces that patch
202
+ atomically only when no intervening canonical or patch publication touched the
203
+ entity. Otherwise it returns a typed `conflict` receipt and leaves current graph
204
+ state unchanged. Fetch/sync metadata-only publications do not cause false
205
+ conflicts. Only one active preview is retained per entity, and receipts are
206
+ discarded with their controller.
207
+
208
+ The same value policy governs events, entity records, and preview receipts.
209
+ Under the default metadata-only policy, value positions contain an explicit
210
+ `{ $type: "hidden-by-policy" }` marker. Hosts must opt into `mode: "include"`
211
+ before canonical values, patches, merged values, or receipt values can cross the
212
+ inspection boundary.
213
+
214
+ The versioned conformance fixture is published at
215
+ `@prometheus-ags/entity-graph-core/devtools/fixtures/entity-inspection-v1.json`.
216
+ Its byte-identical source copy under `packages/entity_graph_flutter` freezes the
217
+ TypeScript/Flutter wire semantics; changing those semantics requires a new
218
+ fixture version.
219
+
220
+ Node ESM consumers import the JSON fixture with an import attribute:
221
+
222
+ ```ts
223
+ import fixture from "@prometheus-ags/entity-graph-core/devtools/fixtures/entity-inspection-v1.json"
224
+ with { type: "json" };
225
+ ```
226
+
227
+ ### Rewind controller-owned snapshot history
228
+
229
+ Each active controller captures one initial attach baseline and the complete
230
+ mutable graph data slice after every semantic graph publication. Snapshot
231
+ payloads, stable cursors, imported candidates, rewind state, and the protected
232
+ live head stay inside that controller; UI code renders status and submits
233
+ intent rather than owning a second history ring.
234
+
235
+ ```ts
236
+ const attachment = attachGraphDevtools(store, {
237
+ storeId: "admin-graph",
238
+ snapshotLimit: 50,
239
+ snapshotBytesLimit: 10 * 1024 * 1024,
240
+ });
241
+
242
+ const controller = attachment.controller;
243
+ if (controller) {
244
+ const status = controller.getSnapshotHistoryStatus();
245
+ const cursor = status.newestCursor;
246
+
247
+ if (cursor !== null) {
248
+ const rewind = controller.rewind(cursor);
249
+ if (rewind?.status === "rewound") {
250
+ // Every graph subscriber now reads the selected historical state.
251
+ controller.returnToLive();
252
+ }
253
+ }
254
+ }
255
+ ```
256
+
257
+ Count and byte ceilings both apply; eviction always removes whole oldest
258
+ snapshots. A cursor is never reused. Rewinding an evicted, cleared, or
259
+ unavailable capture returns a typed `expired-history` receipt with the current
260
+ retained range and never mutates the graph. Oversize captures remain visible as
261
+ `unavailable` metadata rather than retaining a partial graph.
262
+
263
+ The first rewind protects an exact deep clone of the live head. Explicit
264
+ return-to-live restores that clone through the Zustand publication boundary.
265
+ If an application mutation occurs while rewound, DevTools first emits a live
266
+ transition with `reason: "mutation"`, then emits and snapshots the completed
267
+ mutation as the new branch; the former protected future is released.
268
+
269
+ History imports are inert until confirmation. `inspectHistoryImport()` accepts
270
+ only the current protocol version, the same controller store ID, ordered stable
271
+ cursors, JSON-safe complete graph data, and the controller's count/byte budget.
272
+ `confirmHistoryImport(candidateId, cursor)` is one-shot and restores only the
273
+ exact inspected candidate. Inspection receipts and time-travel events expose
274
+ metadata, not snapshot values.
275
+
276
+ The deprecated root functions (`recordGraphSnapshot`,
277
+ `restoreGraphSnapshot*`, `stepTimeTravel`, and `getTimeTravelState`) are a thin
278
+ compatibility facade over the selected store's controller. They own no graph
279
+ payloads or cursors and return unavailable results until the explicit
280
+ `@prometheus-ags/entity-graph-core/devtools` module has been loaded. New tools
281
+ should use the controller's stable cursors and explicit return-to-live API.
282
+
283
+ The versioned time-travel fixture is published at
284
+ `@prometheus-ags/entity-graph-core/devtools/fixtures/time-travel-v1.json`. Its
285
+ byte-identical Flutter source copy freezes retention, rewind/live, branching,
286
+ expired-cursor, and confirmed-import semantics for the later Dart controller.
287
+
288
+ Each store owns one reference-counted controller. Attachments to the same store
289
+ share its event sequence and bounded history; separate stores never share
290
+ events, commands, clients, or teardown. The first active attachment determines
291
+ that controller's identifier, limits, and value policy until its final
292
+ attachment detaches.
293
+
294
+ Set `enabled: false` when a host should skip creating a particular attachment.
295
+ This is an attachment-level no-op, not a global kill switch: it never tears
296
+ down a controller still referenced by another attachment. Detach every active
297
+ attachment to stop observation for that store.
298
+
299
+ The controller observes the Zustand publication boundary, so transactions,
300
+ adapter writes, hydration, rollback, and direct `store.setState` changes use the
301
+ same semantic event stream. Events carry protocol version, store and event IDs,
302
+ sequence/correlation data, before/after graph counts, and categorized changes.
303
+
304
+ The deprecated root `subscribeDevtoolsEvent` API remains the original
305
+ history-free graph-transaction op-site stream. It does not attach this
306
+ controller and does not observe arbitrary direct `store.setState` writes. New
307
+ inspection tools should use this versioned subpath; the separate root shim
308
+ exists only for compatibility and keeps its incremental patch payloads.
309
+
310
+ ### DevTools data boundary
311
+
312
+ Entity and patch values are omitted by default. Events contain metadata and
313
+ counts unless the host explicitly opts into values and supplies any required
314
+ redaction policy:
315
+
316
+ ```ts
317
+ const attachment = attachGraphDevtools(store, {
318
+ values: {
319
+ mode: "include",
320
+ redact(value, context) {
321
+ if (context.category !== "entity" || !value || typeof value !== "object") {
322
+ return value;
323
+ }
324
+ const { accessToken: _secret, ...safe } = value as Record<string, unknown>;
325
+ return safe;
326
+ },
327
+ },
328
+ });
329
+ ```
330
+
331
+ The current v1 redactor receives each whole changed value, represented by an
332
+ empty `context.fieldPath`. `context.destination` distinguishes retained event
333
+ history from on-demand inspection. Nested field paths are reserved for later
334
+ field-level inspection. A throwing redactor marks that change with
335
+ `valueState: "redaction-error"` and exposes neither the exception nor the
336
+ original value.
337
+
338
+ History is bounded by both event count and encoded bytes. Oversized included
339
+ values are removed before an event is retained or delivered; if metadata alone
340
+ still exceeds the per-event limit, `changesOmitted` records the explicit loss.
341
+ Redactor and
342
+ observer failures are isolated from graph writes, disconnected clients cannot
343
+ continue issuing commands, and the controller stops observing the store after
344
+ the final detach. A transport adapter is responsible for preserving the same
345
+ store/client boundary when messages leave the process.
346
+
101
347
  ## Major capabilities
102
348
 
103
349
  - request deduplication, stale-while-revalidate, subscriber tracking, and garbage collection
@@ -116,6 +362,8 @@ The package publishes loader-specific artifacts and declarations:
116
362
 
117
363
  - Node ESM and modern bundlers use `dist/index.mjs` with `dist/index.d.ts`.
118
364
  - Node CommonJS uses `dist/index.cjs` with `dist/index.d.cts`.
365
+ - DevTools ESM uses `dist/devtools.mjs` with `dist/devtools.d.ts`.
366
+ - DevTools CommonJS uses `dist/devtools.cjs` with `dist/devtools.d.cts`.
119
367
 
120
368
  Both paths are selected through conditional package exports.
121
369