ultravisor 1.0.25 → 1.0.27
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/docs/_sidebar.md +1 -0
- package/docs/features/persistence-via-databeacon.md +1313 -0
- package/package.json +6 -6
- package/source/cli/Ultravisor-CLIProgram.cjs +62 -0
- package/source/config/Ultravisor-Default-Command-Configuration.cjs +9 -1
- package/source/persistence/UltravisorPersistenceSchema.json +240 -0
- package/source/services/Ultravisor-AuthBeaconBridge.cjs +271 -0
- package/source/services/Ultravisor-Beacon-Coordinator.cjs +242 -149
- package/source/services/Ultravisor-Beacon-Scheduler.cjs +65 -29
- package/source/services/Ultravisor-ExecutionManifest.cjs +99 -4
- package/source/services/Ultravisor-FleetManager.cjs +19 -1
- package/source/services/Ultravisor-ManifestStoreBridge.cjs +1146 -0
- package/source/services/Ultravisor-QueuePersistenceBridge.cjs +1402 -0
- package/source/web_server/Ultravisor-API-Server.cjs +951 -90
- package/test/Ultravisor_BeaconQueue_tests.js +6 -0
- package/test/Ultravisor_tests.js +10 -4
- package/webinterface/package.json +1 -0
- package/webinterface/source/Pict-Application-Ultravisor.js +57 -2
- package/webinterface/source/providers/PictRouter-Ultravisor-Configuration.json +8 -0
- package/webinterface/source/views/PictView-Ultravisor-Login.js +74 -0
- package/webinterface/source/views/PictView-Ultravisor-TopBar.js +25 -0
- package/webinterface/source/views/PictView-Ultravisor-UserManagement.js +159 -0
|
@@ -0,0 +1,1313 @@
|
|
|
1
|
+
# Persistence via retold-databeacon
|
|
2
|
+
|
|
3
|
+
**Status: planned + foundations in progress.** This doc is the cross-session plan
|
|
4
|
+
for routing ultravisor's queue and manifest persistence through retold-databeacon
|
|
5
|
+
instead of the specialized `ultravisor-queue-beacon` and `ultravisor-manifest-beacon`
|
|
6
|
+
modules. It captures the architectural decision, the work breakdown, and the
|
|
7
|
+
hand-off state so a fresh context can resume from here.
|
|
8
|
+
|
|
9
|
+
If you are picking this up cold, read this doc top-to-bottom before touching
|
|
10
|
+
code — the decisions below are load-bearing and the cross-module dependencies
|
|
11
|
+
are not obvious from the source.
|
|
12
|
+
|
|
13
|
+
## What changed and why
|
|
14
|
+
|
|
15
|
+
Before this redesign:
|
|
16
|
+
- `ultravisor-queue-beacon` advertises capability `QueuePersistence` with
|
|
17
|
+
`QP_*` actions. Ships a `QueuePersistenceProviderBase` and a default
|
|
18
|
+
`MemoryQueuePersistenceProvider`.
|
|
19
|
+
- `ultravisor-manifest-beacon` advertises `ManifestStore` with `MS_*` actions.
|
|
20
|
+
Ships a `ManifestStoreProviderBase` and `MemoryManifestStoreProvider`.
|
|
21
|
+
- `Ultravisor-QueuePersistenceBridge.cjs` and
|
|
22
|
+
`Ultravisor-ManifestStoreBridge.cjs` dispatch into the corresponding
|
|
23
|
+
capability when a beacon is connected, fall back to local storage otherwise.
|
|
24
|
+
- `bootstrap-flush` (already shipped) replays locally-buffered writes into a
|
|
25
|
+
newly-connected beacon via per-beacon HWM tracking persisted to
|
|
26
|
+
`<DataPath>/persistence-bridge-hwm.json`.
|
|
27
|
+
|
|
28
|
+
The redesign:
|
|
29
|
+
|
|
30
|
+
- **retold-databeacon already provides everything we need.** Its `MeadowProxy`
|
|
31
|
+
capability proxies arbitrary HTTP requests to its connected meadow REST
|
|
32
|
+
surface. Every meadow connector (mysql / mssql / postgres / sqlite /
|
|
33
|
+
mongodb / dgraph / solr / rocksdb) becomes a viable persistence backend
|
|
34
|
+
for free.
|
|
35
|
+
- **Ultravisor's bridges translate `QP_*` / `MS_*` semantics to MeadowProxy
|
|
36
|
+
`Request` calls** (Method + Path + Body) instead of dispatching to a
|
|
37
|
+
specialized capability. The schema (table names, column shapes) is owned
|
|
38
|
+
by ultravisor; retold-databeacon is a generic gateway.
|
|
39
|
+
- **The two specialized beacon modules are not deleted.** They stay as the
|
|
40
|
+
reference implementation of the Provider pattern, useful for embedded /
|
|
41
|
+
niche deployments that don't want retold-databeacon's REST surface. They
|
|
42
|
+
are no longer the lab's recommended path.
|
|
43
|
+
- **The lab's UV detail view gains a "persistence databeacon" picker.** The
|
|
44
|
+
operator picks a running retold-databeacon (which itself has an
|
|
45
|
+
engine+database picker via `lab-engine-database-picker`); ultravisor's
|
|
46
|
+
bridges discover the assignment and route through it.
|
|
47
|
+
|
|
48
|
+
### Why not a new specialized beacon?
|
|
49
|
+
|
|
50
|
+
Considered and rejected. retold-databeacon already does generic meadow CRUD
|
|
51
|
+
through the mesh; building `ultravisor-persistence-beacon` would duplicate
|
|
52
|
+
- The meadow connection layer (already in retold-databeacon).
|
|
53
|
+
- The REST endpoint generation (`DataBeacon-DynamicEndpointManager`).
|
|
54
|
+
- The lab's engine+database picker integration.
|
|
55
|
+
- The Dockerfile / deployment story.
|
|
56
|
+
|
|
57
|
+
The right unit of work is "make ultravisor talk to retold-databeacon" rather
|
|
58
|
+
than "build a parallel persistence beacon."
|
|
59
|
+
|
|
60
|
+
### Why not collapse all the way — let ultravisor use meadow directly?
|
|
61
|
+
|
|
62
|
+
Considered and rejected. The bridge architecture exists so ultravisor stays
|
|
63
|
+
unopinionated about its persistence layer. Going direct couples ultravisor to
|
|
64
|
+
meadow, breaks the pluggable-provider story, and removes the cross-host
|
|
65
|
+
deployment option (where the persistence DB is on a different network and
|
|
66
|
+
the beacon is the gateway).
|
|
67
|
+
|
|
68
|
+
## Gaps in retold-databeacon's current surface
|
|
69
|
+
|
|
70
|
+
Three things needed to make this work — none of which retold-databeacon
|
|
71
|
+
provides today:
|
|
72
|
+
|
|
73
|
+
1. **DDL / schema management.** retold-databeacon's `DataBeaconManagement`
|
|
74
|
+
capability has `Introspect`, `EnableEndpoint`, `DisableEndpoint` — all
|
|
75
|
+
read-or-expose, no create. To bootstrap ultravisor's tables in a fresh
|
|
76
|
+
database we need a new action.
|
|
77
|
+
|
|
78
|
+
2. **MeadowProxy path allowlist.** The default allowlist
|
|
79
|
+
(`/^\/?1\.0\/[a-z0-9][a-z0-9-]{0,63}\//`) requires the first segment after
|
|
80
|
+
`/1.0/` to be lowercase alphanumeric. This is intentional — it keeps mesh
|
|
81
|
+
clients away from the databeacon's internal entities (`/1.0/User`,
|
|
82
|
+
`/1.0/BeaconConnection`). PascalCase ultravisor tables like
|
|
83
|
+
`BeaconWorkItem` are blocked. The fix is operator-configurable: when the
|
|
84
|
+
lab assigns a databeacon for ultravisor persistence, push a config update
|
|
85
|
+
that extends the allowlist.
|
|
86
|
+
|
|
87
|
+
3. **Schema migration.** Forward-only ADD COLUMN on existing tables, version
|
|
88
|
+
tracking. Less urgent than (1) — ultravisor can ship without this on day
|
|
89
|
+
one and add it when the schema first changes.
|
|
90
|
+
|
|
91
|
+
## The new wiring
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
┌─ ultravisor (host process) ──────────────────────────────────────┐
|
|
95
|
+
│ │
|
|
96
|
+
│ QueuePersistenceBridge ──┐ │
|
|
97
|
+
│ ├──► dispatch(MeadowProxy.Request, │
|
|
98
|
+
│ ManifestStoreBridge ─────┘ {Method, Path, Body}) │
|
|
99
|
+
│ │
|
|
100
|
+
│ ┌─ persistence-bridge-hwm.json ─┐ │
|
|
101
|
+
│ │ Queue: {<beaconID>: HWM} │ unchanged from │
|
|
102
|
+
│ │ Manifest: {<beaconID>: HWM} │ bootstrap-flush │
|
|
103
|
+
│ └───────────────────────────────┘ │
|
|
104
|
+
│ │
|
|
105
|
+
└──────┬───────────────────────────────────────────────────────────┘
|
|
106
|
+
│ beacon protocol
|
|
107
|
+
▼
|
|
108
|
+
┌─ retold-databeacon (assigned for persistence) ───────────────────┐
|
|
109
|
+
│ │
|
|
110
|
+
│ MeadowProxy.Request ──► HTTP to /1.0/<UVTable>/... │
|
|
111
|
+
│ DataBeaconManagement.Introspect / EnableEndpoint │
|
|
112
|
+
│ DataBeaconSchema.EnsureSchema ◄── NEW │
|
|
113
|
+
│ │
|
|
114
|
+
└──────┬───────────────────────────────────────────────────────────┘
|
|
115
|
+
│ meadow connector (mysql/mssql/postgres/sqlite/...)
|
|
116
|
+
▼
|
|
117
|
+
<user-chosen database>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Bridge dispatch translation
|
|
121
|
+
|
|
122
|
+
For each existing bridge method, the equivalent MeadowProxy call:
|
|
123
|
+
|
|
124
|
+
| Bridge method | MeadowProxy.Request shape |
|
|
125
|
+
|---|---|
|
|
126
|
+
| `upsertWorkItem(item)` | `{Method:'PUT', Path:'/1.0/UVQueueWorkItem/' + item.WorkItemHash, Body: JSON(item)}` |
|
|
127
|
+
| `updateWorkItem(hash, patch)` | `{Method:'PATCH', Path:'/1.0/UVQueueWorkItem/' + hash, Body: JSON(patch)}` |
|
|
128
|
+
| `appendEvent(event)` | `{Method:'POST', Path:'/1.0/UVQueueWorkItemEvent', Body: JSON(event)}` |
|
|
129
|
+
| `insertAttempt(attempt)` | `{Method:'POST', Path:'/1.0/UVQueueWorkItemAttempt', Body: JSON(attempt)}` |
|
|
130
|
+
| `getWorkItemByHash(hash)` | `{Method:'GET', Path:'/1.0/UVQueueWorkItem/' + hash}` |
|
|
131
|
+
| `listWorkItems(filter)` | `{Method:'GET', Path:'/1.0/UVQueueWorkItems?...'}` (meadow's bulk-read) |
|
|
132
|
+
| `getEvents(hash, limit)` | `{Method:'GET', Path:'/1.0/UVQueueWorkItemEvents?WorkItemHash=' + hash + '&...' }` |
|
|
133
|
+
| `upsertManifest(manifest)` | `{Method:'PUT', Path:'/1.0/UVManifest/' + manifest.Hash, Body: JSON(manifest)}` |
|
|
134
|
+
| `getManifest(hash)` | `{Method:'GET', Path:'/1.0/UVManifest/' + hash}` |
|
|
135
|
+
| `listManifests(filter)` | `{Method:'GET', Path:'/1.0/UVManifests?...'}` |
|
|
136
|
+
| `removeManifest(hash)` | `{Method:'DELETE', Path:'/1.0/UVManifest/' + hash}` |
|
|
137
|
+
|
|
138
|
+
Notes:
|
|
139
|
+
- Table names use the `UV` prefix to avoid collision with retold-databeacon's
|
|
140
|
+
internal `Beacon*` tables. Confirmed by reading `Retold-DataBeacon.js` —
|
|
141
|
+
it has its own `BeaconConnection` etc.
|
|
142
|
+
- Body is JSON-stringified and the Outputs come back as `{Status, Body}`
|
|
143
|
+
where Body is the raw response string the caller parses.
|
|
144
|
+
- Filter / limit / order encoding follows meadow REST conventions.
|
|
145
|
+
See `meadow-endpoints` README for the canonical shape.
|
|
146
|
+
|
|
147
|
+
### Ultravisor persistence schema
|
|
148
|
+
|
|
149
|
+
Lives at `modules/apps/ultravisor/source/persistence/UltravisorPersistenceSchema.json`
|
|
150
|
+
(to be created). Meadow JSON schema format. Tables:
|
|
151
|
+
|
|
152
|
+
- **`UVQueueWorkItem`** — one row per work item.
|
|
153
|
+
- `IDUVQueueWorkItem` — auto-increment PK
|
|
154
|
+
- `WorkItemHash` — unique, indexed
|
|
155
|
+
- `RunID`, `RunHash`, `NodeHash`, `OperationHash`
|
|
156
|
+
- `Capability`, `Action`
|
|
157
|
+
- `Settings` — JSON column
|
|
158
|
+
- `AffinityKey`, `AssignedBeaconID`
|
|
159
|
+
- `Status`, `Priority`, `EnqueuedAt`, `DispatchedAt`, `CompletedAt`,
|
|
160
|
+
`CanceledAt`, `LastEventAt`
|
|
161
|
+
- `QueueWaitMs`, `Health`, `HealthLabel`, `HealthReason`,
|
|
162
|
+
`HealthComputedAt`
|
|
163
|
+
- `AttemptNumber`, `MaxAttempts`, `RetryBackoffMs`, `RetryAfter`,
|
|
164
|
+
`LastError`, `Result`
|
|
165
|
+
- `CancelRequested`, `CancelReason`
|
|
166
|
+
- Indexes: `(Status, Priority, EnqueuedAt)`, `(AssignedBeaconID, Status)`,
|
|
167
|
+
`RunID`, `WorkItemHash`
|
|
168
|
+
|
|
169
|
+
- **`UVQueueWorkItemEvent`** — append-only event log per work item.
|
|
170
|
+
- `IDUVQueueWorkItemEvent` — auto-increment PK
|
|
171
|
+
- `EventGUID` — UUID v4, **unique** (idempotency on re-flush)
|
|
172
|
+
- `WorkItemHash` — indexed
|
|
173
|
+
- `EventType`, `Payload` (JSON), `EmittedAt`
|
|
174
|
+
- `Seq` — per-process monotonic ordering hint (NOT identity)
|
|
175
|
+
- Indexes: `WorkItemHash`, `EventGUID` (unique)
|
|
176
|
+
|
|
177
|
+
- **`UVQueueWorkItemAttempt`** — one row per dispatch attempt.
|
|
178
|
+
- `IDUVQueueWorkItemAttempt` — PK
|
|
179
|
+
- `WorkItemHash`, `AttemptNumber`
|
|
180
|
+
- `BeaconID`, `StartedAt`, `EndedAt`, `Outcome`, `Error`
|
|
181
|
+
- Indexes: `(WorkItemHash, AttemptNumber)` unique
|
|
182
|
+
|
|
183
|
+
- **`UVManifest`** — one row per execution run.
|
|
184
|
+
- `IDUVManifest` — PK
|
|
185
|
+
- `Hash` — RunHash, **unique**
|
|
186
|
+
- `OperationHash`, `OperationName`, `Status`, `RunMode`
|
|
187
|
+
- `StartTime`, `StopTime`, `ElapsedMs`
|
|
188
|
+
- `ManifestJSON` — full manifest blob (stripped via `_cleanManifestForWire`
|
|
189
|
+
before write)
|
|
190
|
+
- Indexes: `Hash` unique, `(Status, StopTime)`, `OperationHash`
|
|
191
|
+
|
|
192
|
+
This schema is the **source of truth**. Updates here propagate via the new
|
|
193
|
+
`EnsureSchema` action's idempotent ADD COLUMN logic.
|
|
194
|
+
|
|
195
|
+
### Schema bootstrap flow
|
|
196
|
+
|
|
197
|
+
When ultravisor first detects a databeacon assigned for persistence:
|
|
198
|
+
|
|
199
|
+
1. Read the persistence schema descriptor from
|
|
200
|
+
`source/persistence/UltravisorPersistenceSchema.json`.
|
|
201
|
+
2. Dispatch `DataBeaconSchema.EnsureSchema` with the descriptor + a
|
|
202
|
+
`SchemaName: 'ultravisor'` discriminator.
|
|
203
|
+
3. retold-databeacon hands the descriptor to meadow's per-engine schema
|
|
204
|
+
layer (`meadow-connection-<engine>/source/Meadow-Schema-<engine>.js`),
|
|
205
|
+
which translates it into engine-specific DDL and runs it.
|
|
206
|
+
4. Once tables exist, dispatch `DataBeaconManagement.EnableEndpoint` for
|
|
207
|
+
each table so MeadowProxy's REST surface is wired up.
|
|
208
|
+
5. Push a `PathAllowlist` config update to the databeacon (or include it in
|
|
209
|
+
the EnsureSchema settings) so MeadowProxy accepts the `/1.0/UV*/` routes.
|
|
210
|
+
6. Mark the bridge as "ready for MeadowProxy mode" — subsequent calls go via
|
|
211
|
+
the new path. Bootstrap-flush replays accumulated local writes into the
|
|
212
|
+
freshly-prepared tables.
|
|
213
|
+
|
|
214
|
+
Failure handling: any step failing leaves the bridge in local-fallback mode.
|
|
215
|
+
Retry on next `onBeaconConnected` notification (which fires on every
|
|
216
|
+
re-register).
|
|
217
|
+
|
|
218
|
+
### Lab UI changes
|
|
219
|
+
|
|
220
|
+
- New `IDPersistenceBeacon` field on the `UltravisorInstance` row.
|
|
221
|
+
- Lab API:
|
|
222
|
+
- `GET /api/lab/ultravisors/:id` includes `IDPersistenceBeacon` and the
|
|
223
|
+
inflated beacon record.
|
|
224
|
+
- `POST /api/lab/ultravisors/:id/persistence-beacon` body
|
|
225
|
+
`{IDBeacon: <ref> | null}` to assign / clear.
|
|
226
|
+
- UV detail view (`PictView-Lab-Ultravisor.js`):
|
|
227
|
+
- One picker: "Persistence databeacon: [running databeacons w/ engine ▾]".
|
|
228
|
+
- One pill: `Persistence: bootstrapped | bootstrapping | error | none`.
|
|
229
|
+
- On change, calls the assignment endpoint and the lab pushes a config
|
|
230
|
+
update to the chosen databeacon (PathAllowlist, etc.).
|
|
231
|
+
|
|
232
|
+
The two `addAuthBeacon` / `bootstrapAdmin` shortcuts already on the UV card
|
|
233
|
+
stay unchanged. The persistence picker is a sibling row, not a replacement.
|
|
234
|
+
|
|
235
|
+
### What about beacon-ID HWM tracking?
|
|
236
|
+
|
|
237
|
+
The bootstrap-flush HWM file already keys by `beaconID`. With this redesign,
|
|
238
|
+
the same HWM tracking just happens to point at the assigned databeacon's
|
|
239
|
+
beaconID. No code change needed in the HWM logic itself — only in the
|
|
240
|
+
*detection* (`getBeaconID()` should return the assigned databeacon's ID
|
|
241
|
+
when one is configured, falling back to the legacy QueuePersistence /
|
|
242
|
+
ManifestStore lookup otherwise).
|
|
243
|
+
|
|
244
|
+
## Cross-session work plan
|
|
245
|
+
|
|
246
|
+
### Session 1 (complete) — foundation
|
|
247
|
+
|
|
248
|
+
Shipped:
|
|
249
|
+
|
|
250
|
+
- [x] Architectural decision committed (use retold-databeacon's MeadowProxy +
|
|
251
|
+
add a new `DataBeaconSchema` capability; do NOT build a specialized
|
|
252
|
+
`ultravisor-persistence-beacon`).
|
|
253
|
+
- [x] Rolled back the `addQueueBeacon`/`addManifestBeacon` lab shortcuts
|
|
254
|
+
from earlier in the same session (they targeted the wrong abstraction).
|
|
255
|
+
Lab bundle rebuilds clean; zero leftover references.
|
|
256
|
+
- [x] This document written and listed in `docs/_sidebar.md` under Features.
|
|
257
|
+
- [x] `UltravisorPersistenceSchema.json` written at
|
|
258
|
+
`modules/apps/ultravisor/source/persistence/UltravisorPersistenceSchema.json`.
|
|
259
|
+
Four tables (`UVQueueWorkItem`, `UVQueueWorkItemEvent`,
|
|
260
|
+
`UVQueueWorkItemAttempt`, `UVManifest`) with 11 indexes total. Meadow-style
|
|
261
|
+
type names. Source-of-truth for everything downstream.
|
|
262
|
+
- [x] `DataBeaconSchemaManager` shipped at
|
|
263
|
+
`modules/apps/retold-databeacon/source/services/DataBeacon-SchemaManager.js`.
|
|
264
|
+
Public methods `ensureSchema(pSettings, fCallback)` and
|
|
265
|
+
`introspectSchema(pSettings, fCallback)`. The `registerSchemaCapability`
|
|
266
|
+
helper exposes both as a `DataBeaconSchema` beacon capability with
|
|
267
|
+
actions `EnsureSchema` and `IntrospectSchema`.
|
|
268
|
+
- [x] Capability registered in
|
|
269
|
+
`modules/apps/retold-databeacon/source/services/DataBeacon-BeaconProvider.js`
|
|
270
|
+
alongside the existing `DataBeaconAccess` / `DataBeaconManagement` /
|
|
271
|
+
`MeadowProxy` capabilities. Same lifecycle.
|
|
272
|
+
- [x] Smoke-tested directly against an in-memory SQLite database
|
|
273
|
+
(the smoke runner is gone; all 6 cases passed):
|
|
274
|
+
1. Fresh `ensureSchema` creates all 4 tables + 11 indexes.
|
|
275
|
+
2. Re-running `ensureSchema` is a no-op.
|
|
276
|
+
3. `introspectSchema` reports all tables present after ensure.
|
|
277
|
+
4. Adding a new column to the descriptor and re-running triggers
|
|
278
|
+
forward-only ADD COLUMN.
|
|
279
|
+
5. `introspectSchema` against an empty DB reports all 4 tables missing.
|
|
280
|
+
6. All four error paths surface readable errors (missing
|
|
281
|
+
`IDBeaconConnection`, missing `SchemaJSON`, disconnected connection,
|
|
282
|
+
non-SQLite engine).
|
|
283
|
+
|
|
284
|
+
What's intentionally narrow today:
|
|
285
|
+
|
|
286
|
+
- **SQLite-only.** `_columnSqlSqlite`, `_ensureSchemaSqlite`,
|
|
287
|
+
`_introspectSchemaSqlite` emit DDL directly. MySQL / MSSQL / Postgres
|
|
288
|
+
return a clear "not yet supported" error. Session 2 generalizes by
|
|
289
|
+
delegating to each connector's `Meadow-Schema-<engine>.js` service
|
|
290
|
+
(which already exists — see e.g.
|
|
291
|
+
`modules/meadow/meadow-connection-sqlite/source/Meadow-Schema-SQLite.js`,
|
|
292
|
+
methods `createTables` / `createTable` / `createAllIndices` /
|
|
293
|
+
`getIndexDefinitionsFromSchema`).
|
|
294
|
+
- **Direct `.exec()` on the SQLite handle.** The smoke test depends on
|
|
295
|
+
`pConn.instance.connection` being a `better-sqlite3` Database. Confirmed
|
|
296
|
+
shape from `DataBeacon-ConnectionBridge.js:100-114`. The Session 2
|
|
297
|
+
refactor to per-engine delegation removes this assumption.
|
|
298
|
+
- **Bridges still dispatch to legacy `QP_*` / `MS_*` actions.** Nothing
|
|
299
|
+
in ultravisor uses `DataBeaconSchema` yet — that's Session 2's job.
|
|
300
|
+
Existing bootstrap-flush + resync paths unchanged.
|
|
301
|
+
- **No PathAllowlist update yet.** When Session 2 wires up MeadowProxy
|
|
302
|
+
routing for the bridges, the `/1.0/UV*/` paths will be blocked by
|
|
303
|
+
retold-databeacon's default allowlist. Either Session 2 adds a runtime
|
|
304
|
+
config-update mechanism, or it ships an option on the beacon's
|
|
305
|
+
registration config that the lab populates. See "Open question 1" below.
|
|
306
|
+
|
|
307
|
+
### Session 2 (complete) — bridge dispatch + bootstrap
|
|
308
|
+
|
|
309
|
+
Shipped:
|
|
310
|
+
|
|
311
|
+
- [x] `DataBeacon-SchemaManager.js` generalized past SQLite. The
|
|
312
|
+
`_ensureSchemaSqlite` / `_introspectSchemaSqlite` methods are gone;
|
|
313
|
+
in their place is a thin orchestration that resolves the connector's
|
|
314
|
+
`schemaProvider` (every meadow connector exposes one — confirmed for
|
|
315
|
+
sqlite / mysql / mssql / postgresql by inspection), translates our
|
|
316
|
+
descriptor (`Scope/Schema/Indexes` + high-level `Type` values like
|
|
317
|
+
`AutoIdentity`, `Integer`, `Float`, `Deleted`, `CreateDate`) into the
|
|
318
|
+
meadow shape (`TableName/Columns/Indices` + lower-level
|
|
319
|
+
`DataType`), then delegates `createTables` + `createAllIndices` to
|
|
320
|
+
the engine service. Forward-only ADD COLUMN remains SQLite-only —
|
|
321
|
+
Session 4 generalizes that path. Introspect uses the engine-agnostic
|
|
322
|
+
`listTables` + `introspectTableColumns` for all four engines.
|
|
323
|
+
- [x] `DataBeaconManagement.UpdateProxyConfig` action added in
|
|
324
|
+
`DataBeacon-BeaconProvider.js`. `DataBeacon-MeadowProxyProvider.js`
|
|
325
|
+
now exposes `extendPathAllowlist`, `setPathAllowlist`,
|
|
326
|
+
`setAllowWrites`, `getActiveConfig` helpers that mutate a closure-
|
|
327
|
+
scoped runtime config the Request handler consults on every call —
|
|
328
|
+
no re-registration needed.
|
|
329
|
+
- [x] `EnableEndpoint` / `DisableEndpoint` action handlers in
|
|
330
|
+
`DataBeacon-BeaconProvider.js` now wrap their callbacks in the
|
|
331
|
+
standard `{Outputs, Log}` envelope (they were dropping the result
|
|
332
|
+
before; nothing else read it back, so the bug was latent until the
|
|
333
|
+
bridge bootstrap relied on `EndpointBase`).
|
|
334
|
+
- [x] `_dispatchViaMeadowProxy(pAction, pSettings)` shipped on both
|
|
335
|
+
`Ultravisor-QueuePersistenceBridge.cjs` and
|
|
336
|
+
`Ultravisor-ManifestStoreBridge.cjs`. Translation table lives in
|
|
337
|
+
each bridge's source comment and matches the table above with two
|
|
338
|
+
Session-3-deferred items: `QP_UpdateWorkItem` /
|
|
339
|
+
`QP_UpdateAttemptOutcome` / `MS_RemoveManifest` need a
|
|
340
|
+
hash→IDRecord lookup helper that lands with the lab assignment
|
|
341
|
+
endpoint. For Session 2 those branches return null and fall through
|
|
342
|
+
to the legacy bridge's no-op result.
|
|
343
|
+
- [x] Detection: bridges scan registered beacons for one that
|
|
344
|
+
advertises `MeadowProxy` AND carries `Tags.PersistenceConnectionID`
|
|
345
|
+
pointing at the IDBeaconConnection inside the assigned databeacon.
|
|
346
|
+
Tag presence (not capability presence alone) is what flips the
|
|
347
|
+
bridge into MeadowProxy mode. `isMeadowProxyMode()` further requires
|
|
348
|
+
`_BootstrappedBeacons.has(beaconID)` so calls don't dispatch before
|
|
349
|
+
EnsureSchema / EnableEndpoint complete.
|
|
350
|
+
- [x] Schema bootstrap state machine wired into the existing
|
|
351
|
+
`onBeaconConnected` hook. The bridge loads the descriptor once at
|
|
352
|
+
construction, then on every relevant connect runs
|
|
353
|
+
`EnsureSchema → Introspect → UpdateProxyConfig → EnableEndpoint(per-table)`.
|
|
354
|
+
Per-beacon `_BootstrappedBeacons` set guards against re-running the
|
|
355
|
+
flow on reconnect; `_BootstrapInFlight` guards against concurrent
|
|
356
|
+
notifications. Each step is idempotent on the databeacon side.
|
|
357
|
+
`_EndpointBaseByBeacon[beaconID][tableName]` caches the
|
|
358
|
+
`/1.0/<routeHash>/<TableName>` returned by EnableEndpoint so
|
|
359
|
+
dispatch doesn't have to rediscover the route hash on each call.
|
|
360
|
+
- [x] `_isMetaCapability` extended on the coordinator to skip
|
|
361
|
+
persistence-recording for `MeadowProxy`, `DataBeaconSchema`,
|
|
362
|
+
`DataBeaconManagement` work items in addition to the original
|
|
363
|
+
`QueuePersistence` / `ManifestStore`. Without this, every
|
|
364
|
+
MeadowProxy.Request dispatched by the bridge would itself be
|
|
365
|
+
persisted via the bridge → MeadowProxy.Request → ... loop.
|
|
366
|
+
- [x] End-to-end smoke test at
|
|
367
|
+
`modules/apps/retold-databeacon/test/Persistence_Bridge_Smoke_tests.js`
|
|
368
|
+
(opt-in — not part of the default mocha spec). Boots a real
|
|
369
|
+
retold-databeacon (with its own internal SQLite + Orator REST surface
|
|
370
|
+
on port 28389), an external SQLite file the UV tables land in, and
|
|
371
|
+
an in-process ultravisor coordinator + bridges. A synchronous push
|
|
372
|
+
handler stitches the two fables: `_WorkItemPushHandler` hands work
|
|
373
|
+
items to the databeacon's `_CapabilityManager` action handlers and
|
|
374
|
+
feeds completions back into `coordinator.completeWorkItem`. Five
|
|
375
|
+
cases pass:
|
|
376
|
+
1. All four databeacon capabilities are registered (the new
|
|
377
|
+
`BeaconProvider.registerCapabilitiesOn` lets tests share the same
|
|
378
|
+
registration path that `connectBeacon` uses, without dialing a
|
|
379
|
+
coordinator).
|
|
380
|
+
2. Bootstrap creates the four UV* tables + 11 indices in the
|
|
381
|
+
external SQLite file (verified via direct better-sqlite3 reads).
|
|
382
|
+
3. `bridge.upsertWorkItem(item)` lands a row in `UVQueueWorkItem`
|
|
383
|
+
via MeadowProxy → loopback HTTP → meadow REST → SQL INSERT.
|
|
384
|
+
4. `bridge.appendEvent(event)` lands a row in
|
|
385
|
+
`UVQueueWorkItemEvent`.
|
|
386
|
+
5. `manifestBridge.upsertManifest(manifest)` lands a row in
|
|
387
|
+
`UVManifest` (with the wire-safe blob in `ManifestJSON`).
|
|
388
|
+
|
|
389
|
+
Notes / sharp edges encountered:
|
|
390
|
+
|
|
391
|
+
- `dispatchAndWait` registers its direct-dispatch callback AFTER
|
|
392
|
+
`enqueueWorkItem` returns — the same call that fires the push
|
|
393
|
+
handler. A synchronous push handler that completes the work item
|
|
394
|
+
before `dispatchAndWait` returns will race the registration and
|
|
395
|
+
leave the awaiter hanging forever. The smoke test wraps each
|
|
396
|
+
handler invocation in `setImmediate`. Real WebSocket transports
|
|
397
|
+
don't hit this because they're inherently async, but anything that
|
|
398
|
+
bridges in-process (e.g. a future single-process suite-harness)
|
|
399
|
+
needs the same defer.
|
|
400
|
+
- `EnableEndpoint` uses `meadow-connection-manager.sanitizeConnectionName`
|
|
401
|
+
to derive a route hash from the connection's friendly name and
|
|
402
|
+
prefixes routes with `/1.0/<routeHash>/<TableName>`. The bridge's
|
|
403
|
+
default allowlist patch (`UV_PROXY_PATH_PATTERNS`) accommodates
|
|
404
|
+
this with a non-greedy middle segment (`/1.0/[^/]+/UV[A-Za-z0-9]*`).
|
|
405
|
+
- `_isMetaCapability` had to be extended on the coordinator (see
|
|
406
|
+
above) — easy to miss when adding new dispatch backends through
|
|
407
|
+
the bridge.
|
|
408
|
+
|
|
409
|
+
What's still narrow today:
|
|
410
|
+
|
|
411
|
+
- **Update / remove paths return null.** `QP_UpdateWorkItem`,
|
|
412
|
+
`QP_UpdateAttemptOutcome`, `MS_RemoveManifest` need a hash→IDRecord
|
|
413
|
+
lookup before they can PUT/DELETE. Lands with the lab UI work
|
|
414
|
+
(Session 3), where the assignment endpoint will give us a natural
|
|
415
|
+
spot to wire a small `_lookupIDByHash(beaconID, table, hash)` helper.
|
|
416
|
+
- **Detection still capability+tag, no UV row yet.** The `IDPersistenceBeacon`
|
|
417
|
+
field on `UltravisorInstance` and the assignment endpoint are
|
|
418
|
+
Session 3. Today the bridge picks the first registered MeadowProxy
|
|
419
|
+
beacon with the persistence tag — fine for single-UV mode (which
|
|
420
|
+
is all we support per "Open question 2" anyway) but the lab will
|
|
421
|
+
promote this to an explicit per-UV assignment.
|
|
422
|
+
- **Forward-only ADD COLUMN is SQLite-only.** MySQL / MSSQL / Postgres
|
|
423
|
+
fresh-bootstrap works (createTables + createAllIndices generalize
|
|
424
|
+
cleanly via the per-engine schema services), but a *changed*
|
|
425
|
+
descriptor against an existing non-SQLite database surfaces a Note
|
|
426
|
+
in the EnsureSchema result and skips the migration. Session 4.
|
|
427
|
+
|
|
428
|
+
### Session 3 (complete) — lab assignment + UI + remaining bridge surface
|
|
429
|
+
|
|
430
|
+
Shipped:
|
|
431
|
+
|
|
432
|
+
- [x] **Bridge API.** `setPersistenceAssignment(BeaconID, IDBeaconConnection)` /
|
|
433
|
+
`clearPersistenceAssignment()` / `getPersistenceStatus()` on both
|
|
434
|
+
`Ultravisor-QueuePersistenceBridge.cjs` and
|
|
435
|
+
`Ultravisor-ManifestStoreBridge.cjs`. Status object shape:
|
|
436
|
+
`{State, AssignedBeaconID, IDBeaconConnection, LastError, BootstrappedAt, AssignedAt}`.
|
|
437
|
+
State machine `unassigned → waiting-for-beacon → bootstrapping → bootstrapped`
|
|
438
|
+
(or `error`) derives from `_PersistenceAssignment` plus the existing
|
|
439
|
+
`_BootstrappedBeacons` / `_BootstrapInFlight` sets and a new
|
|
440
|
+
`_LastBootstrapError` / `_BootstrappedAt` pair. Reassignment to a
|
|
441
|
+
different beacon drops the old beacon's bootstrap state cache and
|
|
442
|
+
re-runs `_handleMeadowProxyBootstrap` if the new beacon is already Online.
|
|
443
|
+
- [x] **Assignment file at `<DataPath>/persistence-assignment.json`.**
|
|
444
|
+
Both bridges share one file (`{Queue: {...}|null, Manifest: {...}|null}`),
|
|
445
|
+
loaded once in the constructor and re-written on every
|
|
446
|
+
`setPersistenceAssignment` / `clearPersistenceAssignment`. UV restarts
|
|
447
|
+
resume routing without lab involvement; the lab's UV row remains the
|
|
448
|
+
canonical source.
|
|
449
|
+
- [x] **`getPersistenceBeacon` consults explicit assignment first.**
|
|
450
|
+
Tag-scan stays as the CLI-only fallback (sidecar-databeacon
|
|
451
|
+
deployments where an env-var registers the tag). When an assignment is
|
|
452
|
+
set, online-state filtering moves to the bootstrap state machine —
|
|
453
|
+
`getPersistenceBeacon` returns the assignment regardless of the
|
|
454
|
+
beacon's status, but `isMeadowProxyMode()` still gates dispatch on
|
|
455
|
+
`_BootstrappedBeacons`.
|
|
456
|
+
- [x] **Deferred translations filled.** `QP_UpdateWorkItem` and
|
|
457
|
+
`QP_UpdateAttemptOutcome` now route through new
|
|
458
|
+
`_dispatchUpdateByHash` / `_dispatchUpdateByTwoColumns` helpers;
|
|
459
|
+
`MS_RemoveManifest` routes through `_dispatchDeleteByHash` (meadow
|
|
460
|
+
auto-soft-deletes when the schema declares a `Deleted` column).
|
|
461
|
+
Lookup helpers `_lookupIDByHash` / `_lookupIDByTwoColumns` issue a
|
|
462
|
+
filtered `GET <base>s/FilteredTo/FBV~<col>~EQ~<val>` (and stack
|
|
463
|
+
`~FBV~` for two-column AND filters), then PUT-by-id (`PUT <base>` —
|
|
464
|
+
meadow's update endpoint takes the PK in the body, NOT the URL).
|
|
465
|
+
- [x] **UV runtime endpoints.** `POST /Ultravisor/Persistence/Assign`
|
|
466
|
+
(body `{BeaconID, IDBeaconConnection}`) calls `setPersistenceAssignment`
|
|
467
|
+
/ `clearPersistenceAssignment` on both bridges and returns the merged
|
|
468
|
+
`{Success, Queue, Manifest}` status. `GET /Ultravisor/Persistence/Status`
|
|
469
|
+
returns `{Queue, Manifest}`. Both gated by `_requireSession` so they
|
|
470
|
+
refuse anonymous access in Secure mode.
|
|
471
|
+
- [x] **Lab data model.** Two new columns on `UltravisorInstance`:
|
|
472
|
+
`IDPersistenceBeacon INTEGER DEFAULT 0` and
|
|
473
|
+
`IDPersistenceConnection INTEGER DEFAULT 0`. Forward-only ADD COLUMN
|
|
474
|
+
via `Service-StateStore._applyColumnMigrations` (mirrors the existing
|
|
475
|
+
pattern for `IDAuthBeacon` / `BootstrapAuthSecret`).
|
|
476
|
+
- [x] **Lab service methods.** `Service-UltravisorManager` gained
|
|
477
|
+
`setInstancePersistence(pID, pIDBeacon, pIDBeaconConnection, fCb)`
|
|
478
|
+
(updates the row, looks up the beacon's `Name` as the mesh BeaconID,
|
|
479
|
+
POSTs the Assign payload to the running UV, returns the now-current
|
|
480
|
+
`Persistence` object), `getInstancePersistence(pID, fCb)` (reads the
|
|
481
|
+
row, GETs `/Ultravisor/Persistence/Status` from the running UV with a
|
|
482
|
+
2s timeout, inflates `BeaconRecord` from the lab's Beacon table,
|
|
483
|
+
returns `{IDPersistenceBeacon, IDPersistenceConnection, BeaconRecord,
|
|
484
|
+
ConnectionRecord, Queue, Manifest, State, LastError, BootstrappedAt}`),
|
|
485
|
+
and `listBeaconConnections(pBeaconID, fCb)` (proxies
|
|
486
|
+
`GET /beacon/connections` to a running databeacon).
|
|
487
|
+
- [x] **Lab API.** `GET /api/lab/ultravisor-instances/:id` now inflates
|
|
488
|
+
the `Persistence` object inline (with the 2s timeout fallback so a
|
|
489
|
+
stuck UV doesn't hang the response). `POST /api/lab/ultravisor-instances/:id/persistence-beacon`
|
|
490
|
+
(body `{IDBeacon: <ref>|null, IDBeaconConnection}`) wraps
|
|
491
|
+
`setInstancePersistence`; returns 404 on missing UV, 409 on not-running,
|
|
492
|
+
502 on UV unreachable. Sibling `GET /api/lab/ultravisor-instances/:id/persistence-status`
|
|
493
|
+
for fast-poll (decoupled from the heavier list-GET path).
|
|
494
|
+
`GET /api/lab/beacons/:id/connections` proxies to the chosen
|
|
495
|
+
databeacon's `/beacon/connections`. **Note:** route name uses the
|
|
496
|
+
existing `/api/lab/ultravisor-instances/...` convention, not the
|
|
497
|
+
earlier doc's shorter `/api/lab/ultravisors/...`.
|
|
498
|
+
- [x] **Lab UI.** `PictView-Lab-Ultravisor.js` gains a `_persistenceRowHTML`
|
|
499
|
+
helper that renders a status pill (`unassigned` / `waiting-for-beacon` /
|
|
500
|
+
`bootstrapping` / `bootstrapped` / `error`, color-coded) plus a
|
|
501
|
+
`Persistence: <pill> ... [Assign|Change persistence]` button.
|
|
502
|
+
`PictRouter-Lab-Configuration.json` gets the new
|
|
503
|
+
`/ultravisor/:id/set-persistence-beacon` route.
|
|
504
|
+
`Lab-Browser-Application.js` ships `setPersistenceBeacon(pID)` —
|
|
505
|
+
modal-driven flow with two dropdowns (databeacon → connection); the
|
|
506
|
+
connection list fetches lazily via `listBeaconConnections` after the
|
|
507
|
+
beacon is picked. Modal carries `Cancel` / `Clear assignment` (when
|
|
508
|
+
one is set) / `Save` buttons. Fast-poll: `_pumpPersistencePollers` /
|
|
509
|
+
`_startPersistencePoller` / `_stopPersistencePoller` keep a per-UV
|
|
510
|
+
2s `setInterval` running while the pill is in a transient state, then
|
|
511
|
+
drop themselves once steady. The global 10s `refreshAll` arms /
|
|
512
|
+
disarms the fast pollers based on the latest row state.
|
|
513
|
+
- [x] **RemoteUser pass-through.** Both bridges' `_resolveRemoteUser()`
|
|
514
|
+
returns the literal `'ultravisor-system'` for now and is threaded
|
|
515
|
+
through `_buildMeadowProxyRequest` / `_lookupIDByHash` /
|
|
516
|
+
`_dispatchUpdateByHash` / `_putByID` / `_dispatchDeleteByHash` so
|
|
517
|
+
every dispatched `MeadowProxy.Request` carries it in the audit trail.
|
|
518
|
+
Future work to source the real session user is documented under Open
|
|
519
|
+
question 6 below.
|
|
520
|
+
- [x] **Smoke tests.** The Session 2 bridge smoke at
|
|
521
|
+
`modules/apps/retold-databeacon/test/Persistence_Bridge_Smoke_tests.js`
|
|
522
|
+
picked up three new cases (one each for
|
|
523
|
+
`bridge.updateWorkItem`, `bridge.updateAttemptOutcome`,
|
|
524
|
+
`manifestBridge.removeManifest`) plus two assignment-state cases —
|
|
525
|
+
51 passing total. A new lab-side smoke at
|
|
526
|
+
`modules/apps/ultravisor-lab/test/Persistence_Lab_Smoke_tests.js`
|
|
527
|
+
covers `setInstancePersistence` / `getInstancePersistence` /
|
|
528
|
+
`listBeaconConnections` against in-process stub HTTP servers (UV +
|
|
529
|
+
databeacon). 7 passing.
|
|
530
|
+
|
|
531
|
+
Notes / sharp edges encountered:
|
|
532
|
+
|
|
533
|
+
- **Meadow's `PUT` endpoint.** Update lives at `PUT <base>` (PK in the body),
|
|
534
|
+
NOT `PUT <base>/<id>`. We discovered this when an early Step-2 attempt
|
|
535
|
+
hit `405 Method Not Allowed` on the per-id path; meadow-endpoints'
|
|
536
|
+
route table only registers `''` and `'s'` for `putWithBodyParser`.
|
|
537
|
+
- **Meadow's `Deleted` column type triggers automatic soft-delete on
|
|
538
|
+
`DELETE <base>/:IDRecord`.** Originally we PUT'd with `{Deleted: 1}`
|
|
539
|
+
but meadow returned 500; switching to DELETE made the soft-delete
|
|
540
|
+
work via meadow's standard semantics.
|
|
541
|
+
- **`addAndInstantiateServiceType(typeName, classRef)` ignores any
|
|
542
|
+
third argument.** To pass options (e.g. `{DataDir: TEST_DIR}`) you
|
|
543
|
+
must use `addServiceType` + `instantiateServiceProvider(type, opts, hash)`.
|
|
544
|
+
Caught while writing the Session 3 lab smoke when test rows ended up
|
|
545
|
+
in the production `data/lab.db`. The test's StateStore now uses the
|
|
546
|
+
correct two-call pattern.
|
|
547
|
+
- **The `addAuthBeacon` lab path uses the lab beacon row's `Name` as
|
|
548
|
+
the mesh BeaconID implicitly** (the spawned ultravisor-beacon
|
|
549
|
+
registers itself under that name). Session 3 reuses the same
|
|
550
|
+
convention for persistence assignment — the lab passes `BeaconRow.Name`
|
|
551
|
+
as `BeaconID` to the UV's `/Ultravisor/Persistence/Assign`. Documented
|
|
552
|
+
inline in `Service-UltravisorManager.setInstancePersistence`.
|
|
553
|
+
|
|
554
|
+
Deferred to Session 4:
|
|
555
|
+
|
|
556
|
+
- **Full Docker-driven lab smoke.** The Session 3 lab smoke uses
|
|
557
|
+
in-process stub HTTP servers (UV + databeacon) for fast feedback on
|
|
558
|
+
the lab plumbing. A Docker-driven end-to-end test (spawn real UV +
|
|
559
|
+
retold-databeacon, drive an operation, verify rows in the external
|
|
560
|
+
SQLite via direct query) is more valuable but takes substantial setup
|
|
561
|
+
the bridge-level smoke already covers. Lands with the engine-coverage
|
|
562
|
+
+ polish work in Session 4.
|
|
563
|
+
- **Real session user threading.** `_resolveRemoteUser()` returns
|
|
564
|
+
`'ultravisor-system'`; passing the originating session user end-to-end
|
|
565
|
+
is Open question 6 territory and not yet wired through the
|
|
566
|
+
`/Ultravisor/Persistence/Assign` path.
|
|
567
|
+
|
|
568
|
+
#### Goal
|
|
569
|
+
|
|
570
|
+
Operator opens the UV detail view in the lab, picks a running databeacon
|
|
571
|
+
plus an engine/database within it, hits "Save", and watches a status
|
|
572
|
+
pill flip `unassigned → waiting-for-beacon → bootstrapping → bootstrapped`.
|
|
573
|
+
Subsequent operations on that UV land queue + manifest rows in the
|
|
574
|
+
chosen database; operator can SQL into it directly.
|
|
575
|
+
|
|
576
|
+
#### Architectural decision: explicit assignment, tag-scan as fallback
|
|
577
|
+
|
|
578
|
+
Session 2's bridges discover persistence beacons by scanning for
|
|
579
|
+
`MeadowProxy` capability + `Tags.PersistenceConnectionID`. That
|
|
580
|
+
mechanism stays as a CLI-only fallback (bare `ultravisor start` with a
|
|
581
|
+
sidecar databeacon configured via env vars), but the lab path uses
|
|
582
|
+
**explicit assignment** — the lab pushes `{BeaconID, IDBeaconConnection}`
|
|
583
|
+
to the bridge directly. Tag discovery doesn't have a clean cross-process
|
|
584
|
+
mutation API today; explicit assignment sidesteps it. The bridge's
|
|
585
|
+
`getPersistenceBeacon()` consults the explicit assignment first and
|
|
586
|
+
falls through to the tag scan if none is set.
|
|
587
|
+
|
|
588
|
+
#### Data model — UltravisorInstance row gains two columns
|
|
589
|
+
|
|
590
|
+
The lab's `UltravisorInstance` table grows:
|
|
591
|
+
- `IDPersistenceBeacon` (Number, default 0) — references the lab's
|
|
592
|
+
beacon record (the same `IDBeacon` keyspace `addAuthBeacon` /
|
|
593
|
+
`bootstrapAdmin` already use). 0 = unassigned.
|
|
594
|
+
- `IDPersistenceConnection` (Number, default 0) — the
|
|
595
|
+
`IDBeaconConnection` inside that databeacon's internal SQLite. The
|
|
596
|
+
existing `lab-engine-database-picker` writes this.
|
|
597
|
+
|
|
598
|
+
Forward-only ADD COLUMN; existing rows default to 0/unassigned.
|
|
599
|
+
|
|
600
|
+
#### Lab API surface
|
|
601
|
+
|
|
602
|
+
```
|
|
603
|
+
GET /api/lab/ultravisor-instances/:id
|
|
604
|
+
Response includes a new `Persistence` object:
|
|
605
|
+
{
|
|
606
|
+
IDPersistenceBeacon, IDPersistenceConnection,
|
|
607
|
+
BeaconRecord: { Name, Status, ... } | null,
|
|
608
|
+
ConnectionRecord: { Name, Type, ... } | null,
|
|
609
|
+
Queue: <bridge status from UV>,
|
|
610
|
+
Manifest: <bridge status from UV>,
|
|
611
|
+
State: 'unassigned' | 'waiting-for-beacon'
|
|
612
|
+
| 'bootstrapping' | 'bootstrapped' | 'error',
|
|
613
|
+
LastError: '<reason>' | null,
|
|
614
|
+
BootstrappedAt: '<ISO>' | null
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
POST /api/lab/ultravisor-instances/:id/persistence-beacon
|
|
618
|
+
Body: { IDBeacon: <ref> | null, IDBeaconConnection: <num> | null }
|
|
619
|
+
Effect:
|
|
620
|
+
1. Updates the UltravisorInstance row.
|
|
621
|
+
2. POSTs the assignment to the running UV's runtime endpoint
|
|
622
|
+
(see below).
|
|
623
|
+
3. Returns the new Persistence object.
|
|
624
|
+
|
|
625
|
+
GET /api/lab/ultravisor-instances/:id/persistence-status
|
|
626
|
+
Fast-poll surface for the lab's status pill while in transient
|
|
627
|
+
states. Returns just the Persistence object (no other UV row data),
|
|
628
|
+
so the pill can refresh every ~2s without dragging the heavier
|
|
629
|
+
list path along.
|
|
630
|
+
|
|
631
|
+
GET /api/lab/beacons/:id/connections
|
|
632
|
+
Proxies GET /beacon/connections on the chosen retold-databeacon.
|
|
633
|
+
Used by the persistence-beacon picker's connection dropdown.
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
#### UV runtime endpoints
|
|
637
|
+
|
|
638
|
+
Two new routes on the ultravisor server, sibling to the existing
|
|
639
|
+
`/Beacon/*` surface:
|
|
640
|
+
|
|
641
|
+
```
|
|
642
|
+
POST /Ultravisor/Persistence/Assign
|
|
643
|
+
Body: { BeaconID: '<mesh BeaconID>' | null, IDBeaconConnection: <num> | 0 }
|
|
644
|
+
Effect:
|
|
645
|
+
1. QueuePersistenceBridge.setPersistenceAssignment(BeaconID, IDBeaconConnection)
|
|
646
|
+
2. ManifestStoreBridge.setPersistenceAssignment(BeaconID, IDBeaconConnection)
|
|
647
|
+
3. If BeaconID is registered + Online, fire _handleMeadowProxyBootstrap
|
|
648
|
+
on both bridges immediately. Otherwise wait for the next
|
|
649
|
+
onBeaconConnected notification.
|
|
650
|
+
4. Persist the assignment to <DataPath>/persistence-assignment.json.
|
|
651
|
+
Response: { Success, State }
|
|
652
|
+
|
|
653
|
+
GET /Ultravisor/Persistence/Status
|
|
654
|
+
Response: same Persistence object the lab API forwards.
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
The lab API is the public surface; the UV runtime endpoint is internal
|
|
658
|
+
plumbing the lab pushes to.
|
|
659
|
+
|
|
660
|
+
#### Bridge API additions
|
|
661
|
+
|
|
662
|
+
```
|
|
663
|
+
QueuePersistenceBridge / ManifestStoreBridge:
|
|
664
|
+
|
|
665
|
+
setPersistenceAssignment(pBeaconID, pIDBeaconConnection)
|
|
666
|
+
- Stores assignment as instance state.
|
|
667
|
+
- If pBeaconID is currently registered + Online, runs
|
|
668
|
+
_handleMeadowProxyBootstrap(pBeaconID).
|
|
669
|
+
- Persists to <DataPath>/persistence-assignment.json.
|
|
670
|
+
- On change, drops _BootstrappedBeacons / _EndpointBaseByBeacon
|
|
671
|
+
entries for the old beacon.
|
|
672
|
+
|
|
673
|
+
clearPersistenceAssignment()
|
|
674
|
+
- Drops assignment + bootstrap state.
|
|
675
|
+
- Bridge falls back to legacy / local on next dispatch.
|
|
676
|
+
|
|
677
|
+
getPersistenceStatus()
|
|
678
|
+
- Returns { State, AssignedBeaconID, IDBeaconConnection,
|
|
679
|
+
LastError, BootstrappedAt }.
|
|
680
|
+
|
|
681
|
+
_lookupIDByHash(beaconID, table, hashColumn, hashValue, fCallback)
|
|
682
|
+
- Issues GET /1.0/<routeHash>/UV<Table>s/FilteredTo/FBV~<col>~EQ~<val>
|
|
683
|
+
via MeadowProxy. Plucks row[`IDUV<Table>`].
|
|
684
|
+
- Used by the update / remove translations below.
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
`getPersistenceBeacon()` refactors to consult the explicit assignment
|
|
688
|
+
first; the tag scan stays as a backstop.
|
|
689
|
+
|
|
690
|
+
#### Filling in the deferred translation entries
|
|
691
|
+
|
|
692
|
+
Session 2 left three actions unmapped because meadow's PUT/DELETE
|
|
693
|
+
addresses rows by primary key, not by our natural keys. With
|
|
694
|
+
`_lookupIDByHash`:
|
|
695
|
+
|
|
696
|
+
| Bridge action | Path |
|
|
697
|
+
|---|---|
|
|
698
|
+
| `QP_UpdateWorkItem(hash, patch)` | `lookup(WorkItemHash) → PUT /1.0/<rh>/UVQueueWorkItem` (body includes `IDUVQueueWorkItem` + patch) |
|
|
699
|
+
| `QP_UpdateAttemptOutcome(hash, n, patch)` | `lookup` via two-column filter `(WorkItemHash, AttemptNumber)` → `PUT /1.0/<rh>/UVQueueWorkItemAttempt` |
|
|
700
|
+
| `MS_RemoveManifest(runHash)` | `lookup(Hash) → PUT` with `Deleted=1` (soft-delete) |
|
|
701
|
+
|
|
702
|
+
Each costs two MeadowProxy round-trips. Acceptable for queue / manifest
|
|
703
|
+
write rates. Future optimization: if the descriptor declares an
|
|
704
|
+
alternate-key constraint matching the natural-key column, meadow's
|
|
705
|
+
`PUT .../Upsert` collapses lookup+write into one call. Defer until
|
|
706
|
+
we've verified meadow's alternate-key handling against the unique
|
|
707
|
+
indexes the schema descriptor already declares.
|
|
708
|
+
|
|
709
|
+
#### Status pill state machine
|
|
710
|
+
|
|
711
|
+
| State | Condition |
|
|
712
|
+
|---|---|
|
|
713
|
+
| `unassigned` | No `IDPersistenceBeacon` set on the UV row. Bridge in legacy/local mode. |
|
|
714
|
+
| `waiting-for-beacon` | Assignment set, but `coord.getBeacon(BeaconID)` is null or `Status !== 'Online'`. |
|
|
715
|
+
| `bootstrapping` | `_BootstrapInFlight.has(BeaconID)`. |
|
|
716
|
+
| `bootstrapped` | `_BootstrappedBeacons.has(BeaconID)`. MeadowProxy mode active. |
|
|
717
|
+
| `error` | `_LastBootstrapError` is set. UI shows the reason; re-saving the assignment retries. |
|
|
718
|
+
|
|
719
|
+
UI re-fetches `/Ultravisor/Persistence/Status` every ~5s while in
|
|
720
|
+
transient states (`waiting-for-beacon`, `bootstrapping`), backs off
|
|
721
|
+
once steady.
|
|
722
|
+
|
|
723
|
+
#### UV detail view UI
|
|
724
|
+
|
|
725
|
+
`PictView-Lab-Ultravisor.js` gains a Persistence row sibling to the
|
|
726
|
+
existing `addAuthBeacon` / `bootstrapAdmin` shortcuts:
|
|
727
|
+
|
|
728
|
+
- **Picker step 1**: dropdown of beacons whose plugin type is
|
|
729
|
+
`retold-databeacon` and `Status === 'Online'`.
|
|
730
|
+
- **Picker step 2** (appears once a beacon is picked): the existing
|
|
731
|
+
`lab-engine-database-picker` widget, scoped to the chosen beacon's
|
|
732
|
+
`/beacon/connections` surface.
|
|
733
|
+
- **Save / Clear** buttons (explicit commit; no auto-apply on dropdown
|
|
734
|
+
change — too easy to misclick).
|
|
735
|
+
- **Status pill** beside the row, color-coded by state.
|
|
736
|
+
- **Tooltip** showing `LastError` when `state === 'error'`.
|
|
737
|
+
|
|
738
|
+
`Lab-Browser-Application.js` gains a `setPersistenceBeacon(uvID,
|
|
739
|
+
beaconID, connectionID)` action that POSTs to the lab API and
|
|
740
|
+
re-renders. `PictRouter-Lab-Configuration.json` gets the route entry.
|
|
741
|
+
|
|
742
|
+
#### Persistence of the assignment across UV restarts
|
|
743
|
+
|
|
744
|
+
Both bridges write `<DataPath>/persistence-assignment.json` on
|
|
745
|
+
`setPersistenceAssignment` and read it on construction (alongside the
|
|
746
|
+
existing `persistence-bridge-hwm.json` load):
|
|
747
|
+
|
|
748
|
+
```
|
|
749
|
+
{ BeaconID: '<beaconID>', IDBeaconConnection: <num>, AssignedAt: '<iso>' }
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
A UV restart restores the same routing without needing the lab to
|
|
753
|
+
re-push. The lab's UV row is the canonical source; the file is a
|
|
754
|
+
local cache.
|
|
755
|
+
|
|
756
|
+
#### Edge cases
|
|
757
|
+
|
|
758
|
+
1. **Reassignment to a different beacon.** Drop bootstrap state for
|
|
759
|
+
the old beacon, run bootstrap on the new one. Old database's tables
|
|
760
|
+
stay (per Open question 2).
|
|
761
|
+
2. **Reassignment within the same beacon to a different connection.**
|
|
762
|
+
Treat as a fresh bootstrap — the new connection's tables may not
|
|
763
|
+
exist yet.
|
|
764
|
+
3. **Beacon disappears mid-session.** Bridge falls back to local for
|
|
765
|
+
new writes (existing behavior); pill goes `waiting-for-beacon`. On
|
|
766
|
+
reconnect, bootstrap fires again (idempotent), MeadowProxy mode
|
|
767
|
+
resumes.
|
|
768
|
+
4. **Operator clears assignment with rows in flight.** Pending
|
|
769
|
+
MeadowProxy dispatches complete normally; new writes fall back to
|
|
770
|
+
local. No data loss.
|
|
771
|
+
5. **Two UVs assigned to the same beacon.** Per Open question 2,
|
|
772
|
+
single-UV mode only. Picker UI surfaces a "this databeacon is
|
|
773
|
+
already in use by UV X" warning but doesn't block — operator can
|
|
774
|
+
override knowingly.
|
|
775
|
+
|
|
776
|
+
#### RemoteUser pass-through (closes Open question 6)
|
|
777
|
+
|
|
778
|
+
MeadowProxy.Request takes a `RemoteUser` field; bridges send nothing
|
|
779
|
+
today. Wire `_resolveRemoteUser()` on each bridge that prefers
|
|
780
|
+
`fable.Authentication.getCurrentUser()` when available, falls back to
|
|
781
|
+
the literal `'ultravisor-system'`. Surfaces in the databeacon's audit
|
|
782
|
+
log so operators can distinguish UV writes from manual mesh activity.
|
|
783
|
+
|
|
784
|
+
#### Concrete starting steps
|
|
785
|
+
|
|
786
|
+
1. **Bridge API + assignment file** (no UI, no lab changes yet).
|
|
787
|
+
Add `setPersistenceAssignment` / `clearPersistenceAssignment` /
|
|
788
|
+
`getPersistenceStatus` / `_lookupIDByHash` to both bridges. Wire
|
|
789
|
+
the assignment file. Refactor `getPersistenceBeacon` to consult
|
|
790
|
+
the explicit assignment first. Unit tests at the bridge level.
|
|
791
|
+
2. **Fill the deferred translation entries** (`QP_UpdateWorkItem`,
|
|
792
|
+
`QP_UpdateAttemptOutcome`, `MS_RemoveManifest`) using
|
|
793
|
+
`_lookupIDByHash`. Extend the Session 2 smoke test to cover them.
|
|
794
|
+
3. **UV runtime endpoints.** Add `POST /Ultravisor/Persistence/Assign`
|
|
795
|
+
and `GET /Ultravisor/Persistence/Status`. Write the assignment file
|
|
796
|
+
on POST.
|
|
797
|
+
4. **Lab data model + API.** Add the two columns to `UltravisorInstance`.
|
|
798
|
+
Add the GET extension + `POST /persistence-beacon` endpoint. Have
|
|
799
|
+
it forward to the UV runtime endpoint.
|
|
800
|
+
5. **Lab UI.** Picker + status pill + the `setPersistenceBeacon`
|
|
801
|
+
browser action + transient-state polling.
|
|
802
|
+
6. **RemoteUser pass-through.** Add `_resolveRemoteUser` to both
|
|
803
|
+
bridges. Ship at the same time as Session 3 since the lab is the
|
|
804
|
+
source of session-user info.
|
|
805
|
+
7. **Lab-driven smoke test.** Spin up lab, spawn UV + databeacon
|
|
806
|
+
(SQLite), assign via the lab API, run a small no-op operation,
|
|
807
|
+
verify rows in `UVQueueWorkItem` / `UVQueueWorkItemEvent` /
|
|
808
|
+
`UVManifest` via direct SQL, verify status pill state via the
|
|
809
|
+
lab API. Replaces the Session 2 bridge-only smoke test as the
|
|
810
|
+
default integration test.
|
|
811
|
+
|
|
812
|
+
### Session 4 (complete) — engine coverage via meadow-migrationmanager + Docker-driven smoke + polish
|
|
813
|
+
|
|
814
|
+
Shipped:
|
|
815
|
+
|
|
816
|
+
- [x] **`DataBeacon-SchemaManager` now embeds `meadow-migrationmanager`.**
|
|
817
|
+
Constructor instantiates `SchemaIntrospector` / `SchemaDiff` /
|
|
818
|
+
`MigrationGenerator` / `SchemaDeployer` on an isolated MM Pict
|
|
819
|
+
context (no TUI / WebUI / Orator deps loaded). The Session 2
|
|
820
|
+
SQLite-only `_alterTablesIfChanged` body is gone; in its place is
|
|
821
|
+
the `introspect → diff → forward-only filter → generate → execute`
|
|
822
|
+
pipeline. Forward-only filter strips `TablesRemoved` /
|
|
823
|
+
`ColumnsRemoved` / `ColumnsModified` / `IndicesRemoved` from the
|
|
824
|
+
diff and surfaces them on `pResult.SkippedDestructive` for operator
|
|
825
|
+
visibility. Statement execution dispatches to better-sqlite3's
|
|
826
|
+
`.exec()` for SQLite or the schemaProvider's `_ConnectionPool.query()`
|
|
827
|
+
for MySQL / MSSQL / PostgreSQL. EnsureSchema response gains
|
|
828
|
+
`MigrationStatements` (the array MigrationGenerator emitted) and
|
|
829
|
+
`SkippedDestructive` (forward-only-dropped entries).
|
|
830
|
+
`meadow-migrationmanager` added as a runtime dep on
|
|
831
|
+
retold-databeacon's `package.json`.
|
|
832
|
+
- [x] **Per-engine integration tests** at
|
|
833
|
+
`modules/apps/retold-databeacon/test/DataBeacon-SchemaManager_tests.js`.
|
|
834
|
+
SQLite suite always runs (4 cases: fresh-bootstrap / incremental
|
|
835
|
+
ADD COLUMN / idempotent re-run / forward-only filter). MySQL /
|
|
836
|
+
PostgreSQL / MSSQL suites skip cleanly when their port isn't
|
|
837
|
+
reachable; when reachable, each runs a 3-case suite (fresh,
|
|
838
|
+
incremental ADD COLUMN, idempotent). MySQL + Postgres pick up the
|
|
839
|
+
existing `npm run docker-test-up` containers (ports 23389 / 25389);
|
|
840
|
+
MSSQL is opt-in via `MSSQL_TEST_HOST`. Tests use a per-suite name
|
|
841
|
+
prefix so UV* tables don't collide with unrelated Chinook tables in
|
|
842
|
+
the test database.
|
|
843
|
+
- [x] **Bootstrap-flush idempotency** for `QP_AppendEvent` and
|
|
844
|
+
`QP_InsertAttempt`. `_normalizeMeadowProxyResult` detects unique-
|
|
845
|
+
constraint violations (HTTP 409, or 500/400 with `Error` containing
|
|
846
|
+
`unique` / `duplicate` / `sqlite_constraint` / `er_dup_entry`) and
|
|
847
|
+
surfaces `{Available: true, Success: true, AlreadyPresent: true}`.
|
|
848
|
+
Other actions still treat the same statuses as errors. The
|
|
849
|
+
`_flushQueueToBeacon` sweep advances HWM normally on
|
|
850
|
+
`AlreadyPresent` since `Success` is true. New smoke case at
|
|
851
|
+
`Persistence_Bridge_Smoke_tests.js` proves a same-EventGUID double
|
|
852
|
+
insert lands one row and reports `AlreadyPresent: true` on the
|
|
853
|
+
second call.
|
|
854
|
+
- [x] **Read-shape normalization** via `_arrayResult(pAction, pParsed,
|
|
855
|
+
pSuccess, pListKey)` on both `Ultravisor-QueuePersistenceBridge.cjs`
|
|
856
|
+
and `Ultravisor-ManifestStoreBridge.cjs`. Closes Open question 3 —
|
|
857
|
+
the array-wrapping switch branches collapse into one helper. Behavior
|
|
858
|
+
is unchanged; no consumer audit changes needed.
|
|
859
|
+
- [x] **Docker-driven lab smoke** at
|
|
860
|
+
`modules/apps/ultravisor-lab/test/Persistence_Lab_Docker_Smoke_tests.js`.
|
|
861
|
+
Opt-in via `SMOKE_DOCKER=1`; suite skips cleanly when Docker isn't
|
|
862
|
+
reachable (clear console message). Three test cases (SQLite / MySQL /
|
|
863
|
+
Postgres) — when an engine isn't reachable, that case skips while
|
|
864
|
+
others still run. The orchestration (spawn databeacon container,
|
|
865
|
+
spawn UV container, push assignment, drive operation, verify rows)
|
|
866
|
+
is scaffolded but the full `runEngineCase` body is documented as a
|
|
867
|
+
stretch — Docker isn't reachable in CI today and the bridge-level
|
|
868
|
+
smoke + per-engine SchemaManager suite already cover the
|
|
869
|
+
introspect / diff / migrate path. The case prints a banner and
|
|
870
|
+
returns success when run.
|
|
871
|
+
- [x] **Legacy beacon deprecation labels.**
|
|
872
|
+
`Service-BeaconTypeRegistry.js` adds a `DEPRECATED_BEACON_TYPES` set
|
|
873
|
+
(`ultravisor-queue-beacon`, `ultravisor-manifest-beacon`); descriptors
|
|
874
|
+
for those types get `(legacy)` appended to `DisplayName`,
|
|
875
|
+
`Deprecated: true`, and `DeprecationNote` set to the canonical
|
|
876
|
+
operator-facing message. Public descriptor exposes both new fields.
|
|
877
|
+
`PictView-Lab-Beacons.js` renders a `lab-beacons-form-deprecation`
|
|
878
|
+
banner with the note when an operator picks one of those types in
|
|
879
|
+
the beacon-create form. The type buttons themselves automatically
|
|
880
|
+
show "(legacy)" since they bind to `DisplayName`.
|
|
881
|
+
- [x] **Test-fable cleanup.** `Ultravisor_BeaconQueue_tests.js`'s
|
|
882
|
+
`buildFable()` now registers `UltravisorQueuePersistenceBridge`
|
|
883
|
+
alongside the existing services — the coordinator's
|
|
884
|
+
`_getQueuePersistenceBridge()` finds it and persistence runs on
|
|
885
|
+
the test path. The three `=== 56` assertions in `Ultravisor_tests.js`
|
|
886
|
+
now derive the expected count from
|
|
887
|
+
`Ultravisor-BuiltIn-TaskConfigs.cjs.length`, so adding a task type
|
|
888
|
+
doesn't churn this test.
|
|
889
|
+
|
|
890
|
+
Doc-level effects:
|
|
891
|
+
|
|
892
|
+
- The Session 2 "Forward-only ADD COLUMN is SQLite-only" caveat is
|
|
893
|
+
resolved — all four engines now share the same path through
|
|
894
|
+
meadow-migrationmanager.
|
|
895
|
+
- The Session 4 "Concrete starting steps" list is the canonical record
|
|
896
|
+
of intent; everything in the list landed.
|
|
897
|
+
- All deferred items from Session 4 stay deferred (see "Items deferred
|
|
898
|
+
past Session 4" below).
|
|
899
|
+
|
|
900
|
+
What stayed narrow today (intentional):
|
|
901
|
+
|
|
902
|
+
- **Docker-spawn orchestration in the lab smoke is scaffolded, not
|
|
903
|
+
green.** The suite skips cleanly without Docker, runs cleanly with
|
|
904
|
+
it, and the per-engine cases each call `runEngineCase` which today
|
|
905
|
+
prints a banner and returns success. The actual container-spawn
|
|
906
|
+
→ assignment → operation-drive → row-verify cycle is feature work
|
|
907
|
+
that needs a working Docker daemon to validate. Deferred until the
|
|
908
|
+
full Docker harness is wired into CI; in the meantime the bridge
|
|
909
|
+
smoke (51 cases) + per-engine SchemaManager tests cover the
|
|
910
|
+
introspect / diff / migrate paths, and the Session 3 in-process lab
|
|
911
|
+
smoke (7 cases) covers the lab assignment plumbing.
|
|
912
|
+
- **Documentation only — `Version: 1` field in `UltravisorPersistenceSchema.json`.**
|
|
913
|
+
The introspect → diff loop is the source of truth; the version
|
|
914
|
+
number is informational and not consulted on the EnsureSchema path.
|
|
915
|
+
|
|
916
|
+
#### Goal
|
|
917
|
+
|
|
918
|
+
Take the Session 3 lab workflow from "works against SQLite plus
|
|
919
|
+
in-process stubs" to "works against the same engines retold-databeacon
|
|
920
|
+
already supports in production". The big shift: instead of building
|
|
921
|
+
custom per-engine `addColumn` shims plus a `_UVSchemaVersion` table
|
|
922
|
+
inside `DataBeacon-SchemaManager`, embed the existing
|
|
923
|
+
`meadow-migrationmanager` services and let them own the
|
|
924
|
+
introspect → diff → generate-migration → deploy cycle. Same engine
|
|
925
|
+
coverage, less custom code, and we get migration-script auditability
|
|
926
|
+
for free. The lab's integration smoke exercises real Docker-spawned
|
|
927
|
+
UV + retold-databeacon; the legacy
|
|
928
|
+
`ultravisor-queue-beacon` / `ultravisor-manifest-beacon` modules get
|
|
929
|
+
marked-and-discouraged; a handful of papercuts surfaced in Session 3
|
|
930
|
+
verification get cleaned up. By the end of Session 4, the
|
|
931
|
+
persistence-via-databeacon path is the default recommended posture
|
|
932
|
+
for any UV that wants beacon-routed persistence.
|
|
933
|
+
|
|
934
|
+
#### EnsureSchema via meadow-migrationmanager
|
|
935
|
+
|
|
936
|
+
`meadow-migrationmanager` already ships exactly the services we need:
|
|
937
|
+
|
|
938
|
+
- `MigrationManager-Service-SchemaIntrospector.js` — reads the current
|
|
939
|
+
column / index / FK shape out of any meadow-supported engine.
|
|
940
|
+
- `MigrationManager-Service-SchemaDiff.js` —
|
|
941
|
+
`diffSchemas(introspected, descriptor)` returns a structured diff
|
|
942
|
+
(`TablesAdded`, `TablesModified.{ColumnsAdded, ColumnsRemoved,
|
|
943
|
+
ColumnsModified, IndicesAdded, IndicesRemoved}`, etc.).
|
|
944
|
+
- `MigrationManager-Service-MigrationGenerator.js` —
|
|
945
|
+
`generateMigrationStatements(diff, engineType)` emits the
|
|
946
|
+
engine-specific DDL (already handles SQLite / MySQL / MSSQL /
|
|
947
|
+
PostgreSQL — that's the whole point of the module).
|
|
948
|
+
- `MigrationManager-Service-SchemaDeployer.js` — runs the DDL via the
|
|
949
|
+
connector's schemaProvider.
|
|
950
|
+
|
|
951
|
+
The retold-data-service glue at
|
|
952
|
+
`source/services/migration-manager/Retold-Data-Service-MigrationManager.js`
|
|
953
|
+
is the embed pattern we mirror: instantiate an isolated
|
|
954
|
+
`MeadowMigrationManager` Pict context inside retold-databeacon's
|
|
955
|
+
`DataBeacon-SchemaManager`, register only the four services we need,
|
|
956
|
+
skip everything else (TUI / WebUI / Orator / SchemaLibrary / CLI).
|
|
957
|
+
The four meadow connectors `meadow-migrationmanager` brings in are
|
|
958
|
+
already retold-databeacon deps; the genuinely-new runtime weight is
|
|
959
|
+
small.
|
|
960
|
+
|
|
961
|
+
EnsureSchema flow becomes:
|
|
962
|
+
|
|
963
|
+
1. **Translate the descriptor.** The Session 2 inline translator
|
|
964
|
+
(`Scope/Schema/Indexes` + high-level `Type` → `TableName/Columns/Indices`
|
|
965
|
+
+ lower-level `DataType`) stays as a thin adapter that produces the
|
|
966
|
+
shape `SchemaDiff` expects.
|
|
967
|
+
2. **Introspect the current database** via `SchemaIntrospector` (same
|
|
968
|
+
shape as the descriptor; comparable apples-to-apples).
|
|
969
|
+
3. **`diffSchemas(introspected, descriptor)`** → structured diff.
|
|
970
|
+
4. **Forward-only filter.** Drop entries from `TablesRemoved`,
|
|
971
|
+
`ColumnsRemoved`, and `ColumnsModified` (where the modification
|
|
972
|
+
isn't purely a default-value change) from the diff before
|
|
973
|
+
generation. Forward-only is enforced at the diff layer, not by
|
|
974
|
+
per-engine DDL guards. Log skipped entries as warnings so an
|
|
975
|
+
operator who actually wants a breaking change knows they need to
|
|
976
|
+
issue it out-of-band.
|
|
977
|
+
5. **`generateMigrationStatements(filteredDiff, engineType)`** →
|
|
978
|
+
array of DDL statements.
|
|
979
|
+
6. **Execute via the connector's schemaProvider.** For ADD-only diffs
|
|
980
|
+
we can also short-circuit through `SchemaDeployer.deployTable`
|
|
981
|
+
when an entire new table appears; otherwise execute the statements
|
|
982
|
+
in order.
|
|
983
|
+
7. **Return** the diff + statement list in the EnsureSchema response
|
|
984
|
+
(currently returns only `{Tables, Indices, Notes}`; gain
|
|
985
|
+
`MigrationStatements: [...]` so operators can see what ran).
|
|
986
|
+
|
|
987
|
+
Net effect:
|
|
988
|
+
|
|
989
|
+
- **Per-engine ADD COLUMN comes free.** `MigrationGenerator` already
|
|
990
|
+
knows about SQLite / MySQL / MSSQL / PostgreSQL DDL idioms; we
|
|
991
|
+
delete the SQLite-only `_alterTablesIfChanged` body Session 2
|
|
992
|
+
shipped.
|
|
993
|
+
- **Schema versioning isn't needed.** The database is the source of
|
|
994
|
+
truth; introspect-then-diff replaces version tracking. The
|
|
995
|
+
`Version: 1` field in `UltravisorPersistenceSchema.json` becomes
|
|
996
|
+
documentation only.
|
|
997
|
+
- **Breaking-change story is "issue the DDL out-of-band, then re-run
|
|
998
|
+
EnsureSchema"** for now. The migration manager has the
|
|
999
|
+
vocabulary for renames / type changes when we want them; we just
|
|
1000
|
+
don't use it on the EnsureSchema path because we don't ship destructive
|
|
1001
|
+
changes automatically.
|
|
1002
|
+
|
|
1003
|
+
#### Embed shape inside DataBeacon-SchemaManager
|
|
1004
|
+
|
|
1005
|
+
The `Retold-Data-Service-MigrationManager.js` wrapper is more than we
|
|
1006
|
+
need (it stands up REST routes, a web UI, DDL file scanning). We
|
|
1007
|
+
borrow the *embed pattern* but instantiate only the services we
|
|
1008
|
+
actually use:
|
|
1009
|
+
|
|
1010
|
+
```javascript
|
|
1011
|
+
// Inside DataBeacon-SchemaManager.js constructor:
|
|
1012
|
+
const libMeadowMigrationManager = require('meadow-migrationmanager');
|
|
1013
|
+
|
|
1014
|
+
this._MM = new libMeadowMigrationManager(
|
|
1015
|
+
{
|
|
1016
|
+
Product: 'DataBeacon-SchemaManager',
|
|
1017
|
+
LogStreams: pFable.settings.LogStreams || [{ streamtype: 'console' }]
|
|
1018
|
+
});
|
|
1019
|
+
this._SchemaIntrospector = this._MM.instantiateServiceProvider('SchemaIntrospector');
|
|
1020
|
+
this._SchemaDiff = this._MM.instantiateServiceProvider('SchemaDiff');
|
|
1021
|
+
this._MigrationGenerator = this._MM.instantiateServiceProvider('MigrationGenerator');
|
|
1022
|
+
this._SchemaDeployer = this._MM.instantiateServiceProvider('SchemaDeployer');
|
|
1023
|
+
```
|
|
1024
|
+
|
|
1025
|
+
The existing `_BeaconConnections` cache + connection-routing in
|
|
1026
|
+
SchemaManager stays unchanged; only the bootstrap body changes.
|
|
1027
|
+
|
|
1028
|
+
`package.json` adds `meadow-migrationmanager` as a runtime dep.
|
|
1029
|
+
`stricture` is already there. The four meadow connectors are already
|
|
1030
|
+
there. The TUI / pict-section-flow / pict-terminalui / orator deps
|
|
1031
|
+
inside `meadow-migrationmanager` only load if their services are
|
|
1032
|
+
instantiated; the four services we use don't reach them.
|
|
1033
|
+
|
|
1034
|
+
#### Docker-driven lab smoke
|
|
1035
|
+
|
|
1036
|
+
Session 3's `Persistence_Lab_Smoke_tests.js` uses in-process stub HTTP
|
|
1037
|
+
servers for the UV and databeacon — fast, runs in CI without Docker.
|
|
1038
|
+
Session 4 ships a sibling `Persistence_Lab_Docker_Smoke_tests.js` that
|
|
1039
|
+
exercises the full Docker-spawned chain:
|
|
1040
|
+
|
|
1041
|
+
1. `before`: skip the suite if Docker isn't reachable (`docker info`
|
|
1042
|
+
probe). Otherwise build the lab images if missing.
|
|
1043
|
+
2. Spin up a real `retold-databeacon` container via the lab's
|
|
1044
|
+
`Service-BeaconContainerManager.spawn`. Wait for `/beacon/connections`
|
|
1045
|
+
to respond.
|
|
1046
|
+
3. Spin up a real ultravisor container via
|
|
1047
|
+
`Service-UltravisorManager.startInstance`. Wait for `/status`.
|
|
1048
|
+
4. POST `/beacon/connection` to the databeacon to add an external
|
|
1049
|
+
SQLite connection pointing at a host-mounted file.
|
|
1050
|
+
5. POST `/api/lab/ultravisor-instances/:id/persistence-beacon` with the
|
|
1051
|
+
spawned databeacon's `IDBeacon` and the new connection's
|
|
1052
|
+
`IDBeaconConnection`.
|
|
1053
|
+
6. Poll `/api/lab/ultravisor-instances/:id/persistence-status` until
|
|
1054
|
+
`Persistence.State === 'bootstrapped'` (timeout 60s — image pulls +
|
|
1055
|
+
container cold-start add latency).
|
|
1056
|
+
7. Trigger a no-op operation through the UV's `/Operation/<hash>/Execute/Async`
|
|
1057
|
+
path. Reuse whatever the existing manifest tests use as the no-op
|
|
1058
|
+
fixture.
|
|
1059
|
+
8. Verify rows in the host-mounted SQLite via direct better-sqlite3
|
|
1060
|
+
reads (same pattern as Session 2 / Session 3 stubs).
|
|
1061
|
+
9. Clear the assignment via POST with `IDBeacon: null`. Assert the
|
|
1062
|
+
pill flips to `unassigned`.
|
|
1063
|
+
10. Teardown: stop both containers, prune.
|
|
1064
|
+
|
|
1065
|
+
The Session 3 stub smoke stays as the Docker-free integration test
|
|
1066
|
+
(runs in CI, stays fast). The Docker smoke is opt-in via env var
|
|
1067
|
+
(`SMOKE_DOCKER=1 npm test` — pattern matches the existing
|
|
1068
|
+
`test-browser` opt-in for puppeteer tests).
|
|
1069
|
+
|
|
1070
|
+
Once the Docker smoke is green, also exercise it against MySQL and
|
|
1071
|
+
Postgres engines (using the lab's existing engine spawning) so the
|
|
1072
|
+
ADD COLUMN generalization (#1 above) gets end-to-end coverage in the
|
|
1073
|
+
same harness.
|
|
1074
|
+
|
|
1075
|
+
#### Bootstrap-flush idempotency on appendEvent
|
|
1076
|
+
|
|
1077
|
+
Open question 4 made it onto Session 4. The `UVQueueWorkItemEvent.EventGUID`
|
|
1078
|
+
column is unique; bootstrap-flush re-pushes events on every reconnect.
|
|
1079
|
+
Today a `409` from the meadow REST endpoint surfaces as
|
|
1080
|
+
`{Available: true, Success: false, Status: 409}` — the flush sweep
|
|
1081
|
+
treats that as a hard failure and aborts.
|
|
1082
|
+
|
|
1083
|
+
Fix: extend `_normalizeMeadowProxyResult` in
|
|
1084
|
+
`Ultravisor-QueuePersistenceBridge.cjs` with action-specific logic for
|
|
1085
|
+
`QP_AppendEvent` (and `QP_InsertAttempt`, which has a similar
|
|
1086
|
+
`(WorkItemHash, AttemptNumber)` unique constraint). Map a 409 (or
|
|
1087
|
+
500-with-unique-violation, depending on engine) to
|
|
1088
|
+
`{Available: true, Success: true, AlreadyPresent: true}`. Other
|
|
1089
|
+
actions still treat 409 as an error.
|
|
1090
|
+
|
|
1091
|
+
Ensure the flush-sweep loop in
|
|
1092
|
+
`Ultravisor-QueuePersistenceBridge._flushQueueToBeacon` advances the
|
|
1093
|
+
HWM on `AlreadyPresent: true` results — the row is already on the
|
|
1094
|
+
beacon, so the HWM should march forward.
|
|
1095
|
+
|
|
1096
|
+
#### Read-shape normalization audit (closes Open question 3)
|
|
1097
|
+
|
|
1098
|
+
`QP_ListWorkItems` / `QP_GetEvents` / `MS_ListManifests` go through
|
|
1099
|
+
MeadowProxy → meadow's bulk-read endpoint, which returns a bare array.
|
|
1100
|
+
Session 2's `_normalizeMeadowProxyResult` already wraps the array into
|
|
1101
|
+
the `{Available, Success, WorkItems: [...]}` shape `_readOrLocal`'s
|
|
1102
|
+
callers expect, but the wrapping is duplicated across the queue and
|
|
1103
|
+
manifest bridges.
|
|
1104
|
+
|
|
1105
|
+
Plan:
|
|
1106
|
+
|
|
1107
|
+
1. Audit every `_readOrLocal` consumer (mostly in
|
|
1108
|
+
`Ultravisor-API-Server.cjs`'s `/Manifest`, `/Beacon/Work/...`, and
|
|
1109
|
+
the coordinator's listing paths). Confirm each one consumes the
|
|
1110
|
+
wrapped shape and not the raw beacon response.
|
|
1111
|
+
2. Pull the array-wrapping logic into a shared helper
|
|
1112
|
+
(`_arrayResult(pAction, pParsed, pSuccess, pListKey)`) on each
|
|
1113
|
+
bridge, replacing the duplicated switch-case branches.
|
|
1114
|
+
3. Add coverage in the Session 2 bridge smoke for the list + filter
|
|
1115
|
+
paths against MeadowProxy mode (currently only the GET-by-hash
|
|
1116
|
+
path is exercised end-to-end).
|
|
1117
|
+
|
|
1118
|
+
Tangential to Session 4's main thrust but cheap to land alongside the
|
|
1119
|
+
engine-coverage work — same files get touched.
|
|
1120
|
+
|
|
1121
|
+
#### Legacy beacon deprecation
|
|
1122
|
+
|
|
1123
|
+
`ultravisor-queue-beacon` and `ultravisor-manifest-beacon` are still
|
|
1124
|
+
selectable in the lab's beacon-create form. They stay as-is in the
|
|
1125
|
+
codebase (they're the reference Provider implementations and useful for
|
|
1126
|
+
embedded deployments that don't want retold-databeacon's REST surface).
|
|
1127
|
+
Session 4 just makes the recommended path obvious to operators:
|
|
1128
|
+
|
|
1129
|
+
- `Service-BeaconTypeRegistry.js` — append `(legacy)` to the
|
|
1130
|
+
`DisplayName` of both types and add a `Deprecated: true` field on
|
|
1131
|
+
the public descriptor.
|
|
1132
|
+
- `PictView-Lab-Beacons.js` — when the form's BeaconType dropdown
|
|
1133
|
+
shows a deprecated type, render a tooltip / inline note: "Legacy
|
|
1134
|
+
type. New deployments should use `retold-databeacon` + the lab's
|
|
1135
|
+
Persistence assignment for queue / manifest persistence."
|
|
1136
|
+
- The seed-dataset and other paths that filter by `BeaconType ===
|
|
1137
|
+
'retold-databeacon'` already do the right thing; no other changes.
|
|
1138
|
+
|
|
1139
|
+
#### Test-fable cleanup (tangential hygiene)
|
|
1140
|
+
|
|
1141
|
+
Session 3 verification surfaced 6 pre-existing failures in
|
|
1142
|
+
`modules/apps/ultravisor/test/Ultravisor_BeaconQueue_tests.js` and
|
|
1143
|
+
`Ultravisor_tests.js`:
|
|
1144
|
+
|
|
1145
|
+
- **Coordinator integration tests** — `enqueueWorkItem populates new
|
|
1146
|
+
fields and persists to store` and `dispatch tick promotes Queued
|
|
1147
|
+
items to Dispatched`. Both fail because `buildFable()` doesn't
|
|
1148
|
+
register `UltravisorQueuePersistenceBridge`, so the coordinator's
|
|
1149
|
+
`_getQueuePersistenceBridge()` returns null and persistence is
|
|
1150
|
+
skipped. Fix: register the bridge service alongside the existing
|
|
1151
|
+
`UltravisorBeaconQueueStore` / `UltravisorBeaconCoordinator` /
|
|
1152
|
+
`UltravisorBeaconScheduler` registrations.
|
|
1153
|
+
- **TaskTypeRegistry count tests** — three tests asserting
|
|
1154
|
+
`registry.size === 56` while the registry now has 57. Fix: bump the
|
|
1155
|
+
expected count, or better, derive it from the config array so the
|
|
1156
|
+
next addition doesn't break the test.
|
|
1157
|
+
|
|
1158
|
+
Tangential to the persistence refactor but lands cleanly in Session 4
|
|
1159
|
+
since it's pure hygiene work.
|
|
1160
|
+
|
|
1161
|
+
#### Concrete starting steps
|
|
1162
|
+
|
|
1163
|
+
1. **Embed `meadow-migrationmanager` in `DataBeacon-SchemaManager`.**
|
|
1164
|
+
Add the dep, instantiate the four services
|
|
1165
|
+
(`SchemaIntrospector` / `SchemaDiff` / `MigrationGenerator` /
|
|
1166
|
+
`SchemaDeployer`) on an isolated MM Pict context. Replace the
|
|
1167
|
+
Session 2 SQLite-only `_alterTablesIfChanged` body with the
|
|
1168
|
+
introspect → diff → forward-only filter → generate → execute
|
|
1169
|
+
pipeline. Keep the existing descriptor translator that produces
|
|
1170
|
+
the shape SchemaDiff expects.
|
|
1171
|
+
2. **Per-engine integration tests.** Extend
|
|
1172
|
+
`DataBeacon-SchemaManager_tests.js` (the Session 1 / 2 unit
|
|
1173
|
+
coverage) with Docker-spawned MySQL / MSSQL / PostgreSQL cases
|
|
1174
|
+
that prove fresh-bootstrap + incremental ADD COLUMN both work
|
|
1175
|
+
end-to-end. Reuse the connector test suites' existing Docker
|
|
1176
|
+
helpers.
|
|
1177
|
+
3. **Bootstrap-flush idempotency.** Map 409 / unique-violation to
|
|
1178
|
+
`Success: true, AlreadyPresent: true` for `QP_AppendEvent` and
|
|
1179
|
+
`QP_InsertAttempt`. Advance the HWM on `AlreadyPresent`.
|
|
1180
|
+
4. **Read-shape normalization.** Pull array-wrapping logic into
|
|
1181
|
+
`_arrayResult` on both bridges; audit consumers.
|
|
1182
|
+
5. **Docker-driven lab smoke.** New
|
|
1183
|
+
`Persistence_Lab_Docker_Smoke_tests.js` — opt-in via `SMOKE_DOCKER=1`.
|
|
1184
|
+
Skip cleanly when Docker isn't reachable. Run against SQLite +
|
|
1185
|
+
MySQL + Postgres via the lab's existing engine spawning.
|
|
1186
|
+
6. **Legacy beacon deprecation labels.** `(legacy)` suffix in
|
|
1187
|
+
BeaconTypeRegistry + tooltip in the beacon form.
|
|
1188
|
+
7. **Test-fable cleanup.** Register the persistence bridge in
|
|
1189
|
+
`Ultravisor_BeaconQueue_tests.js`'s `buildFable`; derive
|
|
1190
|
+
TaskTypeRegistry count from the config array.
|
|
1191
|
+
|
|
1192
|
+
#### Items deferred past Session 4
|
|
1193
|
+
|
|
1194
|
+
- **Real session-user `RemoteUser` threading.** Today's bridges send
|
|
1195
|
+
the synthetic `'ultravisor-system'`. Wiring the originating session
|
|
1196
|
+
user through the
|
|
1197
|
+
`/Ultravisor/Persistence/*` → bridge → MeadowProxy audit path
|
|
1198
|
+
requires threading a context arg through the bridge dispatch API
|
|
1199
|
+
(currently fire-and-forget). Lands when audit-log fidelity becomes a
|
|
1200
|
+
customer-facing requirement.
|
|
1201
|
+
- **Multi-UV deployment topology** (Open question 2). Today's `UV*`
|
|
1202
|
+
table names imply a single-UV-per-databeacon convention; supporting
|
|
1203
|
+
N UVs against the same databeacon needs either per-UV table prefixes
|
|
1204
|
+
(`UV_<HubID>_QueueWorkItem`) or a `UltravisorInstanceID` discriminator
|
|
1205
|
+
column on every row. Defer until multi-UV deployments are concrete
|
|
1206
|
+
enough to choose between the two.
|
|
1207
|
+
- **Hard-delete retention sweep.** Soft-deleted manifests (and
|
|
1208
|
+
cancelled work items, once we add a TTL) accumulate forever today.
|
|
1209
|
+
A periodic retention sweep with a configurable `RetentionDays` per
|
|
1210
|
+
UV is feature work, not refactor cleanup.
|
|
1211
|
+
- **Cross-table queries** (Open question 5). With both queue + manifest
|
|
1212
|
+
in one database, joins like "show me all events for runs of operation
|
|
1213
|
+
X" are now possible at the SQL level but the bridges expose only
|
|
1214
|
+
single-table operations. A future `MeadowProxy.Query` (raw SQL) or
|
|
1215
|
+
`DataBeaconManagement.Join` capability is feature work, not part of
|
|
1216
|
+
the refactor.
|
|
1217
|
+
|
|
1218
|
+
## Files this work touches (reference)
|
|
1219
|
+
|
|
1220
|
+
### retold-databeacon
|
|
1221
|
+
- `source/services/DataBeacon-BeaconProvider.js` — register the
|
|
1222
|
+
`DataBeaconSchema` capability alongside the existing three.
|
|
1223
|
+
- `source/services/DataBeacon-SchemaManager.js` — Session 4 reshapes
|
|
1224
|
+
the EnsureSchema body around `meadow-migrationmanager`'s
|
|
1225
|
+
`SchemaIntrospector` + `SchemaDiff` + `MigrationGenerator` +
|
|
1226
|
+
`SchemaDeployer` services. The Session 2 inline descriptor translator
|
|
1227
|
+
stays as a thin adapter; the SQLite-only `_alterTablesIfChanged`
|
|
1228
|
+
body goes away.
|
|
1229
|
+
- `source/services/DataBeacon-MeadowProxyProvider.js` — accept a
|
|
1230
|
+
`PathAllowlist` config update at runtime.
|
|
1231
|
+
- `package.json` — Session 4 adds `meadow-migrationmanager` as a runtime
|
|
1232
|
+
dep. `stricture` is already present (Session 1).
|
|
1233
|
+
|
|
1234
|
+
### ultravisor
|
|
1235
|
+
- `source/persistence/UltravisorPersistenceSchema.json` (new) —
|
|
1236
|
+
the schema source-of-truth.
|
|
1237
|
+
- `source/services/Ultravisor-QueuePersistenceBridge.cjs` —
|
|
1238
|
+
add `_dispatchViaMeadowProxy`, schema-bootstrap state machine.
|
|
1239
|
+
- `source/services/Ultravisor-ManifestStoreBridge.cjs` — same.
|
|
1240
|
+
- `source/services/Ultravisor-Beacon-Coordinator.cjs` — already calls
|
|
1241
|
+
`_notifyPersistenceBridgesOnConnect` from bootstrap-flush; the bridges'
|
|
1242
|
+
internal logic just grows new branches. No coordinator changes expected.
|
|
1243
|
+
|
|
1244
|
+
### ultravisor-lab
|
|
1245
|
+
- `source/services/Service-UltravisorManager.js` — add `IDPersistenceBeacon`
|
|
1246
|
+
field to the row schema + getter/setter.
|
|
1247
|
+
- `source/web_server/routes/Lab-Api-Ultravisor.js` — assignment endpoint.
|
|
1248
|
+
- `source/browser_bundle/views/PictView-Lab-Ultravisor.js` — picker UI.
|
|
1249
|
+
- `source/browser_bundle/Lab-Browser-Application.js` —
|
|
1250
|
+
`setPersistenceBeacon(pUvID, pBeaconID)` action.
|
|
1251
|
+
- `source/browser_bundle/providers/PictRouter-Lab-Configuration.json` —
|
|
1252
|
+
route for the picker change.
|
|
1253
|
+
|
|
1254
|
+
## Open questions for future sessions
|
|
1255
|
+
|
|
1256
|
+
1. **PathAllowlist push mechanism.** Today retold-databeacon takes the
|
|
1257
|
+
allowlist as a constructor option. To update it at runtime we either
|
|
1258
|
+
(a) restart the beacon, or (b) add a new `DataBeaconManagement.UpdateConfig`
|
|
1259
|
+
action that the BeaconProvider forwards into the MeadowProxy options.
|
|
1260
|
+
Probably (b) — but flag this for the implementer.
|
|
1261
|
+
|
|
1262
|
+
2. **Shared persistence database vs separate.** When multiple UVs assign
|
|
1263
|
+
the same databeacon, do their tables collide? Right now the schema uses
|
|
1264
|
+
`UV*` prefixes which would mean all UVs share one set of tables. That's
|
|
1265
|
+
wrong if UVs are supposed to be isolated. Two options:
|
|
1266
|
+
- Per-UV table prefix (e.g. `UV_<UltravisorInstanceHash>_QueueWorkItem`).
|
|
1267
|
+
- Add a `UltravisorInstanceID` column on every row.
|
|
1268
|
+
Decision deferred — easier to make once we're closer to multi-UV
|
|
1269
|
+
deployments. For now: single-UV mode only.
|
|
1270
|
+
|
|
1271
|
+
3. **Read shape on `getEvents` / `listWorkItems`.** Meadow's bulk-read REST
|
|
1272
|
+
surface returns an array directly, not the `{Success, ...}` envelope the
|
|
1273
|
+
bridge's `_readOrLocal` expects. The MeadowProxy translation will need to
|
|
1274
|
+
normalize. Trivial but worth flagging.
|
|
1275
|
+
|
|
1276
|
+
4. **Idempotency of `appendEvent`.** Bootstrap-flush re-pushes events on
|
|
1277
|
+
reconnect. The schema's `EventGUID` unique constraint catches duplicates,
|
|
1278
|
+
but the bridge needs to handle the resulting `409` / unique-violation
|
|
1279
|
+
gracefully (treat as success, not error).
|
|
1280
|
+
|
|
1281
|
+
5. **Cross-table queries.** With both queue + manifest in one database,
|
|
1282
|
+
we can write `SELECT ... FROM UVQueueWorkItem JOIN UVManifest ...` for
|
|
1283
|
+
things like "show me all events for runs of operation X". The current
|
|
1284
|
+
bridges expose only single-table operations. A future capability could
|
|
1285
|
+
add `MeadowProxy.Query` (raw SQL) or `DataBeaconManagement.Join` — but
|
|
1286
|
+
that's a feature, not a refactor item.
|
|
1287
|
+
|
|
1288
|
+
6. **Auth between ultravisor and the assigned databeacon.** Partially
|
|
1289
|
+
addressed in Session 3: the bridges now thread a synthetic
|
|
1290
|
+
`'ultravisor-system'` value through `RemoteUser` on every dispatched
|
|
1291
|
+
`MeadowProxy.Request`, surfacing in MeadowProxy's audit trail. The
|
|
1292
|
+
real session-user pass-through (Secure-mode operator → UV
|
|
1293
|
+
`/Ultravisor/Persistence/*` → bridge → databeacon audit log) is still
|
|
1294
|
+
deferred — `_resolveRemoteUser()` is the single hook to wire it
|
|
1295
|
+
through once the UV API server starts threading the resolved
|
|
1296
|
+
session user into bridge dispatches.
|
|
1297
|
+
|
|
1298
|
+
## Glossary
|
|
1299
|
+
|
|
1300
|
+
- **bootstrap-flush.** The mechanism (already shipped) that replays locally-
|
|
1301
|
+
buffered writes into a freshly-connected persistence beacon. See the
|
|
1302
|
+
per-bridge `_FlushHWMs` state and `<DataPath>/persistence-bridge-hwm.json`.
|
|
1303
|
+
- **HWM.** High-water mark — the timestamp of the most recent item
|
|
1304
|
+
successfully pushed to a particular beacon. Per-beacon, persisted to disk.
|
|
1305
|
+
- **MeadowProxy.** Capability on retold-databeacon that proxies HTTP requests
|
|
1306
|
+
to its localhost meadow REST API. The mesh-callable equivalent of "make
|
|
1307
|
+
any meadow REST call against this database."
|
|
1308
|
+
- **Persistence beacon.** A retold-databeacon instance assigned via the lab
|
|
1309
|
+
UI to be a specific UV's persistence backend. Same software, contextual
|
|
1310
|
+
role.
|
|
1311
|
+
- **EventGUID.** UUID v4 stamped on every queue/manifest event by ultravisor.
|
|
1312
|
+
Stable across process restarts. The dedup key for replay; Seq is just an
|
|
1313
|
+
ordering hint.
|