dsh-vision-router 2.2.1 → 2.2.2

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.
@@ -0,0 +1,331 @@
1
+ import { htmlHasScriptMarker } from './html-script-marker.js'
2
+ import { LOCAL_SETTINGS_PATH } from './dsh-settings-017-compat.js'
3
+
4
+ const SETTINGS_017_MARK = 'data-vision-router-settings-017-compat'
5
+
6
+ export const SETTINGS_017_CLIENT_PRELUDE = String.raw`(function(){
7
+ 'use strict';
8
+ var TARGET = 'dsh-vision-router';
9
+ var ENDPOINT = '${LOCAL_SETTINGS_PATH}';
10
+ var contextCache = typeof WeakMap === 'function' ? new WeakMap() : undefined;
11
+ var scopeCache = typeof WeakMap === 'function' ? new WeakMap() : undefined;
12
+ var binderCache = typeof WeakMap === 'function' ? new WeakMap() : undefined;
13
+ var connectionCache = typeof WeakMap === 'function' ? new WeakMap() : undefined;
14
+
15
+ function safeGet(ctx, name) {
16
+ if (!ctx) return undefined;
17
+ try {
18
+ if (typeof ctx.get === 'function') {
19
+ var value = ctx.get(name);
20
+ if (value !== undefined && value !== null) return value;
21
+ }
22
+ } catch (_) {}
23
+ try { return ctx[name]; } catch (_) { return undefined; }
24
+ }
25
+
26
+ function isLoopbackLocation(locationLike) {
27
+ var hostname = locationLike && typeof locationLike.hostname === 'string'
28
+ ? locationLike.hostname.toLowerCase().replace(/^\[|\]$/g, '')
29
+ : '';
30
+ if (hostname === 'localhost' || hostname.endsWith('.localhost') || hostname === '::1') return true;
31
+ return /^127(?:\.\d{1,3}){3}$/.test(hostname);
32
+ }
33
+
34
+ function normalizeConnection(connection) {
35
+ if (!connection || typeof connection !== 'object') return connection;
36
+ var locationLike;
37
+ try { locationLike = window && window.location; } catch (_) { locationLike = undefined; }
38
+ if (!isLoopbackLocation(locationLike) || connection.isLoopback !== false) return connection;
39
+ if (connectionCache && connectionCache.has(connection)) return connectionCache.get(connection);
40
+ var wrapped = new Proxy(connection, {
41
+ get: function(target, property) {
42
+ if (property === 'isLoopback') return true;
43
+ var value = Reflect.get(target, property, target);
44
+ return typeof value === 'function' ? value.bind(target) : value;
45
+ }
46
+ });
47
+ if (connectionCache) connectionCache.set(connection, wrapped);
48
+ return wrapped;
49
+ }
50
+
51
+ function errorOf(value, fallback) {
52
+ var detail = value && value.error;
53
+ var error = new Error(detail && detail.message ? detail.message : fallback);
54
+ if (detail && detail.code) error.code = detail.code;
55
+ if (detail && detail.details !== undefined) error.details = detail.details;
56
+ return error;
57
+ }
58
+
59
+ function createScope() {
60
+ var listeners = new Set();
61
+ var writeTail = Promise.resolve();
62
+ var inFlight;
63
+ var snapshot = Object.freeze({
64
+ status: 'loading', value: undefined, base: undefined, user: undefined,
65
+ revision: undefined, writable: false, mode: 'host'
66
+ });
67
+ function publish(next) {
68
+ snapshot = Object.freeze(next);
69
+ listeners.forEach(function(listener){ try { listener(); } catch (_) {} });
70
+ }
71
+ function accept(view) {
72
+ if (!view || !view.value || typeof view.value !== 'object' || Array.isArray(view.value)
73
+ || !Number.isInteger(view.revision) || view.revision < 0) {
74
+ throw new Error('Vision Router local settings returned an invalid view');
75
+ }
76
+ publish({
77
+ status: 'ready', value: view.value, base: view.base, user: view.user,
78
+ revision: view.revision, writable: view.writable === true, mode: 'host'
79
+ });
80
+ }
81
+ async function request(method, payload) {
82
+ if (typeof fetch !== 'function') throw new Error('Vision Router local settings transport is unavailable');
83
+ var response = await fetch(ENDPOINT, {
84
+ method: method,
85
+ headers: { accept: 'application/json', ...(method === 'POST' ? { 'content-type': 'application/json' } : {}) },
86
+ cache: 'no-store',
87
+ credentials: 'same-origin',
88
+ ...(method === 'POST' ? { body: JSON.stringify(payload) } : {})
89
+ });
90
+ var body;
91
+ try { body = await response.json(); } catch (_) { body = undefined; }
92
+ if (!response.ok || !body || body.ok !== true) throw errorOf(body, 'Vision Router local settings request failed');
93
+ return body.value;
94
+ }
95
+ function load(restart) {
96
+ if (!restart && inFlight) return inFlight;
97
+ var task = request('GET').then(accept, function(error){
98
+ publish({
99
+ status: 'unavailable', value: undefined, base: undefined, user: undefined,
100
+ revision: undefined, writable: false, mode: 'host', error: error && error.message ? error.message : String(error)
101
+ });
102
+ throw error;
103
+ });
104
+ var held = task.finally(function(){ if (inFlight === held) inFlight = undefined; });
105
+ inFlight = held;
106
+ return held;
107
+ }
108
+ function writeOps(ops) {
109
+ if (!Array.isArray(ops) || ops.length === 0) return Promise.reject(new TypeError('settings operations must be a non-empty array'));
110
+ var task = writeTail.then(async function(){
111
+ if (snapshot.status !== 'ready' || !Number.isInteger(snapshot.revision)) await load(true);
112
+ if (snapshot.status !== 'ready' || !Number.isInteger(snapshot.revision)) throw new Error('Vision Router local settings are not ready');
113
+ if (!snapshot.writable) throw new Error('Vision Router settings provider is read-only');
114
+ try {
115
+ // One ConfigEditor edit may reload DVR. Keep all fields from one UI
116
+ // Save inside that single Host transaction so a second request cannot
117
+ // get stranded between plugin generations.
118
+ var view = await request('POST', { ops: ops, expectedRevision: snapshot.revision });
119
+ accept(view);
120
+ } catch (error) {
121
+ try { await load(true); } catch (_) {}
122
+ throw error;
123
+ }
124
+ });
125
+ writeTail = task.catch(function(){});
126
+ return task;
127
+ }
128
+ function planOps(items) {
129
+ if (!Array.isArray(items) || items.length === 0) throw new TypeError('settings plan must be a non-empty array');
130
+ return items.map(function(item){
131
+ if (!item || typeof item.key !== 'string' || item.key.length === 0 || !item.run) {
132
+ throw new TypeError('settings plan item is invalid');
133
+ }
134
+ return item.run.clear
135
+ ? { op: 'unset', path: [item.key] }
136
+ : { op: 'set', path: [item.key], value: item.run.value };
137
+ });
138
+ }
139
+ var scope = {
140
+ getSnapshot: function(){ return snapshot; },
141
+ subscribe: function(listener){
142
+ if (typeof listener !== 'function') return function(){};
143
+ listeners.add(listener);
144
+ return function(){ listeners.delete(listener); };
145
+ },
146
+ load: function(){ return load(false); },
147
+ reload: function(){ return load(true); },
148
+ set: function(field, value){ return writeOps([{ op: 'set', path: [field], value: value }]); },
149
+ unset: function(field){ return writeOps([{ op: 'unset', path: [field] }]); },
150
+ __visionRouterWritePlan: function(items){ return writeOps(planOps(items)); },
151
+ dispose: async function(){ listeners.clear(); await Promise.allSettled([inFlight, writeTail].filter(Boolean)); }
152
+ };
153
+ void load(false).catch(function(){});
154
+ return scope;
155
+ }
156
+
157
+ function scopeFor(ctx) {
158
+ if (scopeCache && scopeCache.has(ctx)) return scopeCache.get(ctx);
159
+ var scope = createScope();
160
+ if (scopeCache) scopeCache.set(ctx, scope);
161
+ return scope;
162
+ }
163
+
164
+ function syntheticBinder(ctx, original) {
165
+ return {
166
+ bind: function(spec) {
167
+ if (spec && spec.namespace === 'vision-router') return scopeFor(ctx);
168
+ if (original && typeof original.bind === 'function') return original.bind(spec);
169
+ throw new Error('Settings scope ' + String(spec && spec.namespace) + ' is unavailable');
170
+ }
171
+ };
172
+ }
173
+
174
+ function configFormsBinder(ctx) {
175
+ var forms = safeGet(ctx, 'configForms');
176
+ if (!forms || typeof forms.get !== 'function') return undefined;
177
+ if (binderCache && binderCache.has(forms)) return binderCache.get(forms);
178
+ var binder = {
179
+ bind: function(spec) {
180
+ var namespace = spec && spec.namespace;
181
+ if (typeof namespace !== 'string' || namespace.length === 0) {
182
+ throw new TypeError('settings namespace must be a non-empty string');
183
+ }
184
+ return forms.get(namespace);
185
+ }
186
+ };
187
+ if (binderCache) binderCache.set(forms, binder);
188
+ return binder;
189
+ }
190
+
191
+ function wrapContext(ctx) {
192
+ if (!ctx || typeof ctx !== 'object') return ctx;
193
+ if (contextCache && contextCache.has(ctx)) return contextCache.get(ctx);
194
+ var wrapped = new Proxy(ctx, {
195
+ get: function(target, property) {
196
+ if (property === 'settingsScope') {
197
+ var original = safeGet(target, 'settingsScope');
198
+ if (original && typeof original.bind === 'function') return original;
199
+ // DSH 0.1.7 configForms only mirrors Config fields declared volatile.
200
+ // DVR must keep its public Config ordinary for the older supported Host
201
+ // window, so its namespace intentionally uses the local-only ConfigEditor
202
+ // bridge while other namespaces may still delegate to native configForms.
203
+ var official = configFormsBinder(target);
204
+ return syntheticBinder(ctx, official);
205
+ }
206
+ if (property === 'get') {
207
+ var getter = Reflect.get(target, property, target);
208
+ if (typeof getter !== 'function') return getter;
209
+ return function(name) {
210
+ var value = getter.call(target, name);
211
+ return name === 'connection' ? normalizeConnection(value) : value;
212
+ };
213
+ }
214
+ var value = Reflect.get(target, property, target);
215
+ return typeof value === 'function' ? value.bind(target) : value;
216
+ }
217
+ });
218
+ if (contextCache) contextCache.set(ctx, wrapped);
219
+ return wrapped;
220
+ }
221
+
222
+ function requireConfigForms(plugin) {
223
+ if (!plugin || !Array.isArray(plugin.inject)) return plugin;
224
+ var inject = [];
225
+ var inserted = false;
226
+ for (var index = 0; index < plugin.inject.length; index += 1) {
227
+ var service = plugin.inject[index];
228
+ if (service === 'settingsScope') {
229
+ if (!inserted) {
230
+ inject.push('configForms');
231
+ inserted = true;
232
+ }
233
+ continue;
234
+ }
235
+ if (service === 'configForms') inserted = true;
236
+ if (inject.indexOf(service) === -1) inject.push(service);
237
+ }
238
+ // Another loader compatibility layer may already have removed the legacy
239
+ // dependency before this factory runs. DSH 0.1.7 still requires the native
240
+ // configForms service to be activation-ready before Vision Router applies.
241
+ if (!inserted) inject.unshift('configForms');
242
+ try {
243
+ plugin.inject = inject;
244
+ return plugin;
245
+ } catch (_) {
246
+ return Object.assign({}, plugin, { inject: inject });
247
+ }
248
+ }
249
+
250
+ function patchLoader(loader) {
251
+ if (!loader || typeof loader.load !== 'function' || loader.load.__visionRouterSettings017Compat) return;
252
+ var original = loader.load;
253
+ function load(spec) {
254
+ if (spec && spec.id === TARGET && typeof spec.factory === 'function') {
255
+ var factory = spec.factory;
256
+ spec = Object.assign({}, spec, {
257
+ factory: function(require) {
258
+ var plugin = requireConfigForms(factory(require));
259
+ if (plugin && typeof plugin.apply === 'function' && !plugin.apply.__visionRouterSettings017Compat) {
260
+ var apply = plugin.apply;
261
+ var wrappedApply = function(ctx) {
262
+ var rest = Array.prototype.slice.call(arguments, 1);
263
+ return apply.apply(plugin, [wrapContext(ctx)].concat(rest));
264
+ };
265
+ Object.defineProperty(wrappedApply, '__visionRouterSettings017Compat', { value: true });
266
+ plugin.apply = wrappedApply;
267
+ }
268
+ return plugin;
269
+ }
270
+ });
271
+ }
272
+ return original.call(this, spec);
273
+ }
274
+ Object.defineProperty(load, '__visionRouterSettings017Compat', { value: true });
275
+ loader.load = load;
276
+ }
277
+
278
+ function patchCreate(loader) {
279
+ if (!loader || typeof loader.create !== 'function' || loader.create.__visionRouterSettings017Compat) return;
280
+ var original = loader.create;
281
+ function create() {
282
+ var result = original.apply(this, arguments);
283
+ patchLoader(loader);
284
+ if (result && result !== loader) patchLoader(result);
285
+ return result;
286
+ }
287
+ Object.defineProperty(create, '__visionRouterSettings017Compat', { value: true });
288
+ loader.create = create;
289
+ if (loader.mode === 'live') patchLoader(loader);
290
+ }
291
+
292
+ function install() {
293
+ var descriptor = Object.getOwnPropertyDescriptor(window, '__ModuleLoader__');
294
+ var stored = window.__ModuleLoader__;
295
+ if (stored) patchCreate(stored);
296
+ if (descriptor && descriptor.configurable === false) return;
297
+ var previousGet = descriptor && descriptor.get;
298
+ var previousSet = descriptor && descriptor.set;
299
+ Object.defineProperty(window, '__ModuleLoader__', {
300
+ configurable: true,
301
+ enumerable: !descriptor || descriptor.enumerable !== false,
302
+ get: function(){ return previousGet ? previousGet.call(window) : stored; },
303
+ set: function(value) {
304
+ if (previousSet) previousSet.call(window, value); else stored = value;
305
+ try { patchCreate(previousGet ? previousGet.call(window) : value); } catch (_) {}
306
+ }
307
+ });
308
+ }
309
+
310
+ try { install(); } catch (_) {}
311
+ })();`
312
+
313
+ export function injectSettings017ClientPrelude(html) {
314
+ if (typeof html !== 'string' || htmlHasScriptMarker(html, SETTINGS_017_MARK)) return html
315
+ const safe = SETTINGS_017_CLIENT_PRELUDE.replace(/<\/script/gi, '<\\/script')
316
+ const script = `<script ${SETTINGS_017_MARK}>${safe}</script>`
317
+ const closeHead = html.indexOf('</head>')
318
+ return closeHead === -1 ? `${html}${script}` : `${html.slice(0, closeHead)}${script}${html.slice(closeHead)}`
319
+ }
320
+
321
+ export function installSettings017ClientCompatibility(ctx) {
322
+ if (!ctx || typeof ctx.inject !== 'function') return
323
+ // ConfigEditor is the generation fence: old supported Hosts keep their native
324
+ // settingsScope activation contract and never see this client shim.
325
+ ctx.inject(['configEditor', 'webServer'], (webCtx) => {
326
+ webCtx.effect(
327
+ () => webCtx.webServer.tapIndex(injectSettings017ClientPrelude),
328
+ 'vision-router: DSH 0.1.7 settings client compatibility',
329
+ )
330
+ })
331
+ }
@@ -31,6 +31,14 @@ export const SETTINGS_RC8_CLIENT_PRELUDE = String.raw`(function(){
31
31
  return undefined;
32
32
  }
33
33
 
34
+ function isLoopbackLocation(locationLike) {
35
+ var hostname = locationLike && typeof locationLike.hostname === 'string'
36
+ ? locationLike.hostname.toLowerCase().replace(/^\[|\]$/g, '')
37
+ : '';
38
+ if (hostname === 'localhost' || hostname.endsWith('.localhost') || hostname === '::1') return true;
39
+ return /^127(?:\.\d{1,3}){3}$/.test(hostname);
40
+ }
41
+
34
42
  async function writePermission(operation, value, revision) {
35
43
  if (typeof fetch !== 'function') throw new Error('Vision Router local settings transport is unavailable');
36
44
  var payload = { operation: operation };
@@ -269,6 +277,11 @@ export const SETTINGS_RC8_CLIENT_PRELUDE = String.raw`(function(){
269
277
  if (connectionCache && connectionCache.has(connection)) return connectionCache.get(connection);
270
278
  var wrapped = new Proxy(connection, {
271
279
  get: function(target, property) {
280
+ if (property === 'isLoopback') {
281
+ var locationLike;
282
+ try { locationLike = window && window.location; } catch (_) { locationLike = undefined; }
283
+ if (isLoopbackLocation(locationLike)) return true;
284
+ }
272
285
  if (property === 'rpc') return wrapRpc(Reflect.get(target, property, target));
273
286
  var value = Reflect.get(target, property, target);
274
287
  return typeof value === 'function' ? value.bind(target) : value;
@@ -0,0 +1,299 @@
1
+ import {
2
+ DSH_017_SETTINGS_COMPAT_MARK,
3
+ LOCAL_SETTINGS_PATH,
4
+ } from '../dsh-settings-017-compat.js'
5
+
6
+ const BODY_LIMIT_BYTES = 256 * 1024
7
+ const ROOT_TRANSPORT_REGISTRY_KEY = Symbol.for('dsh-vision-router.settings-017-root-local-transport')
8
+ const SLOW_MUTATION_MS = 5_000
9
+ const TRACE_ENV = 'DVR_SETTINGS_017_TRACE'
10
+
11
+ function objectLike(value) {
12
+ return value !== null && typeof value === 'object'
13
+ }
14
+
15
+ function trace(stage, detail) {
16
+ if (process.env[TRACE_ENV] !== '1') return
17
+ try {
18
+ console.warn(`vision-router: DSH 0.1.7 settings trace ${stage}${detail === undefined ? '' : ` ${JSON.stringify(detail)}`}`)
19
+ } catch {}
20
+ }
21
+
22
+ function transportRegistry() {
23
+ let registry = globalThis[ROOT_TRANSPORT_REGISTRY_KEY]
24
+ if (!(registry instanceof WeakMap)) {
25
+ registry = new WeakMap()
26
+ Object.defineProperty(globalThis, ROOT_TRANSPORT_REGISTRY_KEY, {
27
+ value: registry,
28
+ configurable: true,
29
+ })
30
+ }
31
+ return registry
32
+ }
33
+
34
+ function rootOf(ctx) {
35
+ try {
36
+ if (objectLike(ctx?.root)) return ctx.root
37
+ } catch {}
38
+ return ctx
39
+ }
40
+
41
+ function stateFor(root) {
42
+ const registry = transportRegistry()
43
+ let state = registry.get(root)
44
+ if (!state) {
45
+ state = {
46
+ current: undefined,
47
+ routeOwner: undefined,
48
+ routeInstalling: false,
49
+ generationSequence: 0,
50
+ requestSequence: 0,
51
+ }
52
+ registry.set(root, state)
53
+ }
54
+ return state
55
+ }
56
+
57
+ function serviceOf(ctx, name) {
58
+ try {
59
+ const value = typeof ctx?.get === 'function' ? ctx.get(name) : undefined
60
+ if (value !== undefined && value !== null) return value
61
+ } catch {}
62
+ try {
63
+ const value = ctx?.[name]
64
+ return value === undefined || value === null ? undefined : value
65
+ } catch {
66
+ return undefined
67
+ }
68
+ }
69
+
70
+ function compatibleSettings(ctx) {
71
+ const settings = serviceOf(ctx, 'settings')
72
+ if (settings?.[DSH_017_SETTINGS_COMPAT_MARK] !== true) return undefined
73
+ if (typeof settings.describe !== 'function' || typeof settings.mutate !== 'function') return undefined
74
+ return settings
75
+ }
76
+
77
+ function namespaceDescriptor(settings) {
78
+ const descriptors = settings.describe({ redactSecrets: true })
79
+ return Array.isArray(descriptors) ? descriptors.find((entry) => entry?.ns === 'vision-router') : undefined
80
+ }
81
+
82
+ function sendJson(res, status, body) {
83
+ res.writeHead(status, {
84
+ 'content-type': 'application/json; charset=utf-8',
85
+ 'cache-control': 'no-store',
86
+ })
87
+ res.end(JSON.stringify(body))
88
+ }
89
+
90
+ async function readJson(req) {
91
+ let size = 0
92
+ const chunks = []
93
+ for await (const chunk of req) {
94
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
95
+ size += bytes.length
96
+ if (size > BODY_LIMIT_BYTES) throw Object.assign(new Error('request body too large'), { statusCode: 413 })
97
+ chunks.push(bytes)
98
+ }
99
+ if (chunks.length === 0) return {}
100
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'))
101
+ }
102
+
103
+ function currentSettings(state) {
104
+ const generation = state.current
105
+ if (!generation || generation.active === false) return undefined
106
+ try {
107
+ return generation.resolve()
108
+ } catch {
109
+ return undefined
110
+ }
111
+ }
112
+
113
+ function flattenEffectLabels(effects) {
114
+ const labels = []
115
+ const visit = (rows) => {
116
+ if (!Array.isArray(rows)) return
117
+ for (const row of rows) {
118
+ if (!row || typeof row !== 'object') continue
119
+ if (typeof row.label === 'string') labels.push(row.label)
120
+ visit(row.children)
121
+ }
122
+ }
123
+ visit(effects)
124
+ return labels
125
+ }
126
+
127
+ function slowMutationSnapshot(root) {
128
+ const loader = serviceOf(root, 'loader')
129
+ let entries = []
130
+ try { entries = typeof loader?.entries === 'function' ? [...loader.entries()] : [] } catch {}
131
+ return entries.flatMap((entry) => {
132
+ const fiber = entry?.fiber
133
+ if (!fiber || (entry?.options?.id !== 'vision-router' && !fiber.inertia)) return []
134
+ let effects = []
135
+ try { effects = typeof fiber.getEffects === 'function' ? flattenEffectLabels(fiber.getEffects()) : [] } catch {}
136
+ return [{
137
+ id: entry?.options?.id,
138
+ state: fiber.state,
139
+ inertia: Boolean(fiber.inertia),
140
+ effects,
141
+ }]
142
+ })
143
+ }
144
+
145
+ function beginSlowMutationWatch(root, requestId) {
146
+ return setTimeout(() => {
147
+ const snapshot = slowMutationSnapshot(root)
148
+ const message = `vision-router: DSH 0.1.7 settings mutation still reconciling after ${SLOW_MUTATION_MS}ms request=${requestId} ${JSON.stringify(snapshot)}`
149
+ try {
150
+ if (root?.logger && typeof root.logger.warn === 'function') root.logger.warn(message)
151
+ else console.warn(message)
152
+ } catch {
153
+ try { console.warn(message) } catch {}
154
+ }
155
+ }, SLOW_MUTATION_MS)
156
+ }
157
+
158
+ function routeFor(root, state) {
159
+ return {
160
+ kind: 'exact',
161
+ path: LOCAL_SETTINGS_PATH,
162
+ async handler(req, res) {
163
+ const requestId = ++state.requestSequence
164
+ trace('handler-enter', { requestId, method: req.method, generation: state.current?.id })
165
+ const settings = currentSettings(state)
166
+ if (!settings) {
167
+ trace('settings-unavailable', { requestId })
168
+ sendJson(res, 404, { ok: false, error: { code: 'settings-unavailable', message: 'Vision Router settings are unavailable' } })
169
+ return
170
+ }
171
+
172
+ if (req.method === 'GET') {
173
+ const descriptor = namespaceDescriptor(settings)
174
+ if (!descriptor) {
175
+ sendJson(res, 404, { ok: false, error: { code: 'settings-unavailable', message: 'Vision Router settings are unavailable' } })
176
+ return
177
+ }
178
+ sendJson(res, 200, { ok: true, value: { ...descriptor, writable: settings.writable === true } })
179
+ trace('response', { requestId, status: 200, revision: descriptor.revision })
180
+ return
181
+ }
182
+
183
+ if (req.method !== 'POST') {
184
+ res.setHeader('Allow', 'GET, POST')
185
+ sendJson(res, 405, { ok: false, error: { code: 'method-not-allowed', message: 'method not allowed' } })
186
+ return
187
+ }
188
+
189
+ let slowTimer
190
+ try {
191
+ trace('body-read-start', { requestId })
192
+ const payload = await readJson(req)
193
+ trace('body-read-done', { requestId, expectedRevision: payload?.expectedRevision, ops: payload?.ops?.length })
194
+ slowTimer = beginSlowMutationWatch(root, requestId)
195
+ trace('mutate-start', { requestId, generation: state.current?.id })
196
+ await settings.mutate('vision-router', payload?.ops, payload?.expectedRevision)
197
+ trace('mutate-done', { requestId, generation: state.current?.id })
198
+
199
+ // ConfigEditor.edit() reconciles the profile and can dispose the plugin
200
+ // generation that accepted this request. The route itself is owned by a
201
+ // root child fiber that depends only on WebServer, so finish the response
202
+ // from the newly-mounted DVR generation when present.
203
+ const afterSettings = currentSettings(state) ?? settings
204
+ const descriptor = namespaceDescriptor(afterSettings)
205
+ if (!descriptor) throw new Error('Vision Router settings disappeared after the write')
206
+ sendJson(res, 200, { ok: true, value: { ...descriptor, writable: afterSettings.writable === true } })
207
+ trace('response', { requestId, status: 200, revision: descriptor.revision })
208
+ } catch (error) {
209
+ const conflict = error?.code === 'SETTINGS_CONFLICT'
210
+ const status = error?.statusCode ?? (conflict ? 409 : 400)
211
+ trace('response-error', { requestId, status, code: error?.code, message: error?.message ?? String(error) })
212
+ sendJson(res, status, {
213
+ ok: false,
214
+ error: {
215
+ code: conflict ? 'settings-conflict' : 'settings-rejected',
216
+ message: error?.message ?? String(error),
217
+ ...(conflict ? { details: { expected: error.expected, actual: error.actual } } : {}),
218
+ },
219
+ })
220
+ } finally {
221
+ if (slowTimer) clearTimeout(slowTimer)
222
+ }
223
+ },
224
+ }
225
+ }
226
+
227
+ function ensureRootRoute(root, state) {
228
+ if (state.routeOwner || state.routeInstalling) return
229
+ if (typeof root?.inject !== 'function') {
230
+ throw new Error('Vision Router requires root dependency injection for the DSH 0.1.7 local settings route')
231
+ }
232
+
233
+ state.routeInstalling = true
234
+ try {
235
+ const owner = root.inject(['webServer'], (webCtx) => {
236
+ if (!webCtx?.webServer || typeof webCtx.webServer.register !== 'function' || typeof webCtx.effect !== 'function') {
237
+ throw new Error('Vision Router requires WebServer route registration on DSH 0.1.7')
238
+ }
239
+ trace('route-register', {})
240
+ webCtx.effect(
241
+ () => {
242
+ const dispose = webCtx.webServer.register(routeFor(root, state))
243
+ return () => {
244
+ trace('route-dispose', {})
245
+ dispose()
246
+ }
247
+ },
248
+ 'vision-router: DSH 0.1.7 root local settings transport',
249
+ )
250
+ })
251
+ // `root.inject()` creates a root child fiber. It is independent from the
252
+ // DVR plugin fiber and therefore survives ConfigEditor reconciliation of
253
+ // the `vision-router` entry. Keep the owner reachable for the process life.
254
+ state.routeOwner = owner ?? true
255
+ } catch (error) {
256
+ state.routeOwner = undefined
257
+ throw error
258
+ } finally {
259
+ state.routeInstalling = false
260
+ }
261
+ }
262
+
263
+ /**
264
+ * ConfigEditor.edit() reconciles the plugin configuration and therefore disposes
265
+ * the DVR plugin fiber while a Settings POST is still in flight. A dedicated
266
+ * root child fiber owns the HTTP route and depends only on WebServer; individual
267
+ * DVR generations only publish a resolver for their current settings facade.
268
+ */
269
+ export function installDsh017RootLocalSettingsTransport(ctx) {
270
+ if (!ctx || typeof ctx.inject !== 'function') return
271
+ const root = rootOf(ctx)
272
+ if (!objectLike(root)) return
273
+ const state = stateFor(root)
274
+
275
+ ctx.inject(['settings'], (settingsCtx) => {
276
+ const mounted = compatibleSettings(settingsCtx)
277
+ if (!mounted) return
278
+
279
+ const generation = {
280
+ id: ++state.generationSequence,
281
+ active: true,
282
+ resolve: () => compatibleSettings(ctx) ?? mounted,
283
+ }
284
+ state.current = generation
285
+ trace('generation-mount', { generation: generation.id })
286
+ ensureRootRoute(root, state)
287
+
288
+ if (typeof settingsCtx?.effect === 'function') {
289
+ settingsCtx.effect(
290
+ () => () => {
291
+ generation.active = false
292
+ if (state.current === generation) state.current = undefined
293
+ trace('generation-dispose', { generation: generation.id })
294
+ },
295
+ 'vision-router: DSH 0.1.7 settings generation',
296
+ )
297
+ }
298
+ })
299
+ }