@ai-matrx/summon 0.0.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/LICENSE +21 -0
- package/README.md +379 -1
- package/dist/closeBinding-C6raC3qJ.d.cts +20 -0
- package/dist/closeBinding-C6raC3qJ.d.ts +20 -0
- package/dist/dialog.cjs +60 -0
- package/dist/dialog.cjs.map +1 -0
- package/dist/dialog.d.cts +16 -0
- package/dist/dialog.d.ts +16 -0
- package/dist/dialog.js +38 -0
- package/dist/dialog.js.map +1 -0
- package/dist/errors-eJrKXq33.d.cts +30 -0
- package/dist/errors-eJrKXq33.d.ts +30 -0
- package/dist/handles.cjs +411 -0
- package/dist/handles.cjs.map +1 -0
- package/dist/handles.d.cts +166 -0
- package/dist/handles.d.ts +166 -0
- package/dist/handles.js +388 -0
- package/dist/handles.js.map +1 -0
- package/dist/index.cjs +2383 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1259 -0
- package/dist/index.d.ts +1259 -0
- package/dist/index.js +2363 -0
- package/dist/index.js.map +1 -0
- package/dist/lite.cjs +460 -0
- package/dist/lite.cjs.map +1 -0
- package/dist/lite.d.cts +56 -0
- package/dist/lite.d.ts +56 -0
- package/dist/lite.js +444 -0
- package/dist/lite.js.map +1 -0
- package/dist/next.cjs +61 -0
- package/dist/next.cjs.map +1 -0
- package/dist/next.d.cts +5 -0
- package/dist/next.d.ts +5 -0
- package/dist/next.js +39 -0
- package/dist/next.js.map +1 -0
- package/dist/persistence-data.cjs +143 -0
- package/dist/persistence-data.cjs.map +1 -0
- package/dist/persistence-data.d.cts +72 -0
- package/dist/persistence-data.d.ts +72 -0
- package/dist/persistence-data.js +120 -0
- package/dist/persistence-data.js.map +1 -0
- package/dist/persistence.cjs +1073 -0
- package/dist/persistence.cjs.map +1 -0
- package/dist/persistence.d.cts +236 -0
- package/dist/persistence.d.ts +236 -0
- package/dist/persistence.js +1077 -0
- package/dist/persistence.js.map +1 -0
- package/dist/react.cjs +738 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +266 -0
- package/dist/react.d.ts +266 -0
- package/dist/react.js +723 -0
- package/dist/react.js.map +1 -0
- package/dist/sheet.cjs +71 -0
- package/dist/sheet.cjs.map +1 -0
- package/dist/sheet.d.cts +17 -0
- package/dist/sheet.d.ts +17 -0
- package/dist/sheet.js +49 -0
- package/dist/sheet.js.map +1 -0
- package/dist/styles.css +78 -0
- package/dist/testing.cjs +132 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +44 -0
- package/dist/testing.d.ts +44 -0
- package/dist/testing.js +115 -0
- package/dist/testing.js.map +1 -0
- package/dist/url.cjs +362 -0
- package/dist/url.cjs.map +1 -0
- package/dist/url.d.cts +153 -0
- package/dist/url.d.ts +153 -0
- package/dist/url.js +352 -0
- package/dist/url.js.map +1 -0
- package/dist/window-host.cjs +123 -0
- package/dist/window-host.cjs.map +1 -0
- package/dist/window-host.d.cts +72 -0
- package/dist/window-host.d.ts +72 -0
- package/dist/window-host.js +104 -0
- package/dist/window-host.js.map +1 -0
- package/dist/window.cjs +3977 -0
- package/dist/window.cjs.map +1 -0
- package/dist/window.d.cts +796 -0
- package/dist/window.d.ts +796 -0
- package/dist/window.js +4002 -0
- package/dist/window.js.map +1 -0
- package/package.json +239 -1
|
@@ -0,0 +1,1073 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/persistence.ts
|
|
22
|
+
var persistence_exports = {};
|
|
23
|
+
__export(persistence_exports, {
|
|
24
|
+
DEFAULT_IDB_READ_BUDGET_MS: () => DEFAULT_IDB_READ_BUDGET_MS,
|
|
25
|
+
DEFAULT_SAVE_DEBOUNCE_MS: () => DEFAULT_SAVE_DEBOUNCE_MS,
|
|
26
|
+
DEFAULT_WINDOW_DATA_BYTES: () => DEFAULT_WINDOW_DATA_BYTES,
|
|
27
|
+
LOAD_TIMEOUT_ERROR: () => LOAD_TIMEOUT_ERROR,
|
|
28
|
+
MAX_PERSISTED_WINDOW_SESSIONS: () => MAX_PERSISTED_WINDOW_SESSIONS,
|
|
29
|
+
MAX_WINDOW_WORKSPACE_BYTES: () => MAX_WINDOW_WORKSPACE_BYTES,
|
|
30
|
+
MAX_WORKSPACES_PER_IDENTITY: () => MAX_WORKSPACES_PER_IDENTITY,
|
|
31
|
+
OverlayPersistence: () => OverlayPersistence,
|
|
32
|
+
PERSON_WORKSPACE_KEY: () => PERSON_WORKSPACE_KEY,
|
|
33
|
+
PersistenceLoadTimeoutError: () => PersistenceLoadTimeoutError,
|
|
34
|
+
PersistenceVersionConflictError: () => PersistenceVersionConflictError,
|
|
35
|
+
STATE_KEY_PREFIX: () => STATE_KEY_PREFIX,
|
|
36
|
+
VERSION_CONFLICT_ERROR: () => VERSION_CONFLICT_ERROR,
|
|
37
|
+
WINDOW_WORKSPACE_SCHEMA_VERSION: () => WINDOW_WORKSPACE_SCHEMA_VERSION,
|
|
38
|
+
WORKSPACE_LEASE_MS: () => WORKSPACE_LEASE_MS,
|
|
39
|
+
createIndexedDbBackend: () => createIndexedDbBackend,
|
|
40
|
+
createLocalPersistence: () => createLocalPersistence,
|
|
41
|
+
createMemoryBackend: () => createMemoryBackend,
|
|
42
|
+
createOverlayPersistenceController: () => createOverlayPersistenceController,
|
|
43
|
+
hydrateWindowWorkspace: () => hydrateWindowWorkspace,
|
|
44
|
+
identityScopedKey: () => identityScopedKey,
|
|
45
|
+
isLoadTimeout: () => isLoadTimeout,
|
|
46
|
+
isLocalPersistence: () => isLocalPersistence,
|
|
47
|
+
isVersionConflict: () => isVersionConflict,
|
|
48
|
+
persistsComponentState: () => persistsComponentState,
|
|
49
|
+
preservationFor: () => preservationFor,
|
|
50
|
+
readSession: () => readSession,
|
|
51
|
+
sanitizeJsonRecord: () => sanitizeJsonRecord,
|
|
52
|
+
serializeWindowWorkspace: () => serializeWindowWorkspace,
|
|
53
|
+
stateKey: () => stateKey,
|
|
54
|
+
tabWorkspaceKey: () => tabWorkspaceKey,
|
|
55
|
+
useOverlayPersistence: () => useOverlayPersistence
|
|
56
|
+
});
|
|
57
|
+
module.exports = __toCommonJS(persistence_exports);
|
|
58
|
+
|
|
59
|
+
// src/persistence/backend.ts
|
|
60
|
+
function createMemoryBackend() {
|
|
61
|
+
const data = /* @__PURE__ */ new Map();
|
|
62
|
+
return {
|
|
63
|
+
data,
|
|
64
|
+
get: async (key) => data.has(key) ? structuredClone(data.get(key)) : void 0,
|
|
65
|
+
set: async (key, value) => {
|
|
66
|
+
data.set(key, structuredClone(value));
|
|
67
|
+
},
|
|
68
|
+
delete: async (key) => {
|
|
69
|
+
data.delete(key);
|
|
70
|
+
},
|
|
71
|
+
keys: async (prefix) => [...data.keys()].filter((key) => key.startsWith(prefix))
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function request(req) {
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
req.onsuccess = () => resolve(req.result);
|
|
77
|
+
req.onerror = () => reject(req.error ?? new Error("IndexedDB request failed"));
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function createIndexedDbBackend(dbName = "ai-matrx-overlays", storeName = "kv") {
|
|
81
|
+
let opening = null;
|
|
82
|
+
const db = () => {
|
|
83
|
+
if (opening) return opening;
|
|
84
|
+
opening = new Promise((resolve, reject) => {
|
|
85
|
+
if (typeof indexedDB === "undefined") {
|
|
86
|
+
reject(new Error("IndexedDB is unavailable"));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const req = indexedDB.open(dbName, 1);
|
|
90
|
+
req.onupgradeneeded = () => {
|
|
91
|
+
if (!req.result.objectStoreNames.contains(storeName)) req.result.createObjectStore(storeName);
|
|
92
|
+
};
|
|
93
|
+
req.onsuccess = () => {
|
|
94
|
+
const conn = req.result;
|
|
95
|
+
conn.onversionchange = () => {
|
|
96
|
+
conn.close();
|
|
97
|
+
opening = null;
|
|
98
|
+
};
|
|
99
|
+
resolve(conn);
|
|
100
|
+
};
|
|
101
|
+
req.onerror = () => reject(req.error ?? new Error("IndexedDB open failed"));
|
|
102
|
+
req.onblocked = () => reject(new Error("IndexedDB open blocked"));
|
|
103
|
+
}).catch((error) => {
|
|
104
|
+
opening = null;
|
|
105
|
+
throw error;
|
|
106
|
+
});
|
|
107
|
+
return opening;
|
|
108
|
+
};
|
|
109
|
+
const run = async (mode, body) => {
|
|
110
|
+
const conn = await db();
|
|
111
|
+
const tx = conn.transaction(storeName, mode);
|
|
112
|
+
const done = mode === "readwrite" ? new Promise((resolve, reject) => {
|
|
113
|
+
tx.oncomplete = () => resolve();
|
|
114
|
+
tx.onerror = () => reject(tx.error ?? new Error("IndexedDB transaction failed"));
|
|
115
|
+
tx.onabort = () => reject(tx.error ?? new Error("IndexedDB transaction aborted"));
|
|
116
|
+
}) : Promise.resolve();
|
|
117
|
+
const result = await request(body(tx.objectStore(storeName)));
|
|
118
|
+
await done;
|
|
119
|
+
return result;
|
|
120
|
+
};
|
|
121
|
+
return {
|
|
122
|
+
get: (key) => run("readonly", (store) => store.get(key)),
|
|
123
|
+
set: async (key, value) => {
|
|
124
|
+
await run("readwrite", (store) => store.put(value, key));
|
|
125
|
+
},
|
|
126
|
+
delete: async (key) => {
|
|
127
|
+
await run("readwrite", (store) => store.delete(key));
|
|
128
|
+
},
|
|
129
|
+
keys: async (prefix) => {
|
|
130
|
+
const range = IDBKeyRange.bound(prefix, `${prefix}\uFFFF`);
|
|
131
|
+
const keys = await run("readonly", (store) => store.getAllKeys(range));
|
|
132
|
+
return keys.filter((key) => typeof key === "string");
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/persistence/errors.ts
|
|
138
|
+
var VERSION_CONFLICT_ERROR = "OverlayPersistenceVersionConflict";
|
|
139
|
+
var LOAD_TIMEOUT_ERROR = "OverlayPersistenceLoadTimeout";
|
|
140
|
+
var PersistenceVersionConflictError = class extends Error {
|
|
141
|
+
key;
|
|
142
|
+
expectedVersion;
|
|
143
|
+
currentVersion;
|
|
144
|
+
constructor(key, expectedVersion, currentVersion) {
|
|
145
|
+
super(`[overlays/persistence] "${key}" is at version ${currentVersion}, not the expected ${expectedVersion}`);
|
|
146
|
+
this.name = VERSION_CONFLICT_ERROR;
|
|
147
|
+
this.key = key;
|
|
148
|
+
this.expectedVersion = expectedVersion;
|
|
149
|
+
this.currentVersion = currentVersion;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
function isVersionConflict(error) {
|
|
153
|
+
return error instanceof Error && error.name === VERSION_CONFLICT_ERROR;
|
|
154
|
+
}
|
|
155
|
+
var PersistenceLoadTimeoutError = class extends Error {
|
|
156
|
+
key;
|
|
157
|
+
pending;
|
|
158
|
+
constructor(key, budgetMs, pending) {
|
|
159
|
+
super(`[overlays/persistence] "${key}" did not load within ${budgetMs}ms`);
|
|
160
|
+
this.name = LOAD_TIMEOUT_ERROR;
|
|
161
|
+
this.key = key;
|
|
162
|
+
this.pending = pending;
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
function isLoadTimeout(error) {
|
|
166
|
+
return error instanceof Error && error.name === LOAD_TIMEOUT_ERROR;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// src/persistence/keys.ts
|
|
170
|
+
var PERSON_WORKSPACE_KEY = "workspace";
|
|
171
|
+
var tabWorkspaceKey = (workspaceId) => `workspace:${workspaceId}`;
|
|
172
|
+
var STATE_KEY_PREFIX = "state:";
|
|
173
|
+
var stateKey = (instanceKey) => `${STATE_KEY_PREFIX}${instanceKey}`;
|
|
174
|
+
var identityScopedKey = (identityKey, key) => `${identityKey}/${key}`;
|
|
175
|
+
|
|
176
|
+
// src/persistence/localStore.ts
|
|
177
|
+
var DEFAULT_IDB_READ_BUDGET_MS = 750;
|
|
178
|
+
var WORKSPACE_LEASE_MS = 24 * 60 * 60 * 1e3;
|
|
179
|
+
var MAX_WORKSPACES_PER_IDENTITY = 5;
|
|
180
|
+
var INDEX_REFRESH_MS = 60 * 1e3;
|
|
181
|
+
var RECORD_VERSION = 1;
|
|
182
|
+
function isStoredRecord(value) {
|
|
183
|
+
return typeof value === "object" && value !== null && value.v === RECORD_VERSION && typeof value.version === "number" && "value" in value;
|
|
184
|
+
}
|
|
185
|
+
function randomId() {
|
|
186
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
187
|
+
return `tab-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
188
|
+
}
|
|
189
|
+
var browserStorage = (pick) => {
|
|
190
|
+
try {
|
|
191
|
+
return typeof window === "undefined" ? null : window[pick];
|
|
192
|
+
} catch {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
var defaultIsReload = () => {
|
|
197
|
+
try {
|
|
198
|
+
const entry = performance.getEntriesByType("navigation")[0];
|
|
199
|
+
return entry?.type === "reload";
|
|
200
|
+
} catch {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
function isLocalPersistence(port) {
|
|
205
|
+
return Boolean(port && "workspaces" in port && typeof port.workspaces === "object");
|
|
206
|
+
}
|
|
207
|
+
function createLocalPersistence(options = {}) {
|
|
208
|
+
const ns = options.namespace ?? "ai-matrx.overlays";
|
|
209
|
+
const storage = options.storage === void 0 ? browserStorage("localStorage") : options.storage;
|
|
210
|
+
const session = options.session === void 0 ? browserStorage("sessionStorage") : options.session;
|
|
211
|
+
const locks = options.locks === void 0 ? typeof navigator !== "undefined" && navigator.locks ? navigator.locks : null : options.locks;
|
|
212
|
+
const backend = options.backend ?? (typeof indexedDB !== "undefined" ? createIndexedDbBackend(ns) : createMemoryBackend());
|
|
213
|
+
const budgetMs = options.idbReadBudgetMs ?? DEFAULT_IDB_READ_BUDGET_MS;
|
|
214
|
+
const now = options.now ?? Date.now;
|
|
215
|
+
const isReload = options.isReload ?? defaultIsReload;
|
|
216
|
+
const runtimeId = randomId();
|
|
217
|
+
const fallbackWorkspaceId = `fallback-${runtimeId}`;
|
|
218
|
+
const queues = /* @__PURE__ */ new Map();
|
|
219
|
+
let leased = null;
|
|
220
|
+
const mirrorKey = (key) => `${ns}:${key}`;
|
|
221
|
+
const leaseKey = (workspaceId) => `${ns}:lease:${workspaceId}`;
|
|
222
|
+
const indexPrefix = (identityKey) => `${ns}:index:${identityKey}:`;
|
|
223
|
+
const readJson = (key) => {
|
|
224
|
+
if (!storage) return null;
|
|
225
|
+
try {
|
|
226
|
+
const raw = storage.getItem(key);
|
|
227
|
+
return raw === null ? null : JSON.parse(raw);
|
|
228
|
+
} catch {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
const writeJson = (key, value) => {
|
|
233
|
+
if (!storage) return false;
|
|
234
|
+
try {
|
|
235
|
+
storage.setItem(key, JSON.stringify(value));
|
|
236
|
+
return true;
|
|
237
|
+
} catch {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
const removeKey = (key) => {
|
|
242
|
+
try {
|
|
243
|
+
storage?.removeItem(key);
|
|
244
|
+
} catch {
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
const storageKeys = (prefix) => {
|
|
248
|
+
if (!storage) return [];
|
|
249
|
+
const out = [];
|
|
250
|
+
try {
|
|
251
|
+
for (let i = 0; i < storage.length; i += 1) {
|
|
252
|
+
const key = storage.key(i);
|
|
253
|
+
if (key?.startsWith(prefix)) out.push(key);
|
|
254
|
+
}
|
|
255
|
+
} catch {
|
|
256
|
+
}
|
|
257
|
+
return out;
|
|
258
|
+
};
|
|
259
|
+
const readMirror = (key) => {
|
|
260
|
+
const raw = readJson(mirrorKey(key));
|
|
261
|
+
return isStoredRecord(raw) ? raw : null;
|
|
262
|
+
};
|
|
263
|
+
const enqueue = (key, operation) => {
|
|
264
|
+
const previous = queues.get(key) ?? Promise.resolve();
|
|
265
|
+
const next = previous.catch(() => void 0).then(() => locks ? locks.request(`${ns}:${key}`, operation) : operation());
|
|
266
|
+
queues.set(key, next);
|
|
267
|
+
void next.then(
|
|
268
|
+
() => {
|
|
269
|
+
if (queues.get(key) === next) queues.delete(key);
|
|
270
|
+
},
|
|
271
|
+
() => {
|
|
272
|
+
if (queues.get(key) === next) queues.delete(key);
|
|
273
|
+
}
|
|
274
|
+
);
|
|
275
|
+
return next;
|
|
276
|
+
};
|
|
277
|
+
const readWarm = async (key) => {
|
|
278
|
+
const raw = await backend.get(key);
|
|
279
|
+
return isStoredRecord(raw) ? { value: raw.value, version: raw.version } : null;
|
|
280
|
+
};
|
|
281
|
+
const leaseBody = (workspaceId) => {
|
|
282
|
+
const raw = readJson(leaseKey(workspaceId));
|
|
283
|
+
return typeof raw === "object" && raw !== null ? raw : null;
|
|
284
|
+
};
|
|
285
|
+
const hasLiveLease = (workspaceId, at) => {
|
|
286
|
+
const body = leaseBody(workspaceId);
|
|
287
|
+
return typeof body?.expiresAt === "number" && body.expiresAt > at;
|
|
288
|
+
};
|
|
289
|
+
const claimLease = (workspaceId) => {
|
|
290
|
+
if (leased && leased !== workspaceId && leaseBody(leased)?.runtimeId === runtimeId) removeKey(leaseKey(leased));
|
|
291
|
+
leased = workspaceId;
|
|
292
|
+
writeJson(leaseKey(workspaceId), { runtimeId, expiresAt: now() + WORKSPACE_LEASE_MS });
|
|
293
|
+
};
|
|
294
|
+
const workspaces = {
|
|
295
|
+
currentId() {
|
|
296
|
+
if (!session) {
|
|
297
|
+
claimLease(fallbackWorkspaceId);
|
|
298
|
+
return fallbackWorkspaceId;
|
|
299
|
+
}
|
|
300
|
+
try {
|
|
301
|
+
const sessionKey = `${ns}:workspace-id`;
|
|
302
|
+
const existing = session.getItem(sessionKey);
|
|
303
|
+
const body = existing ? leaseBody(existing) : null;
|
|
304
|
+
const claimedElsewhere = Boolean(
|
|
305
|
+
existing && !isReload() && body?.runtimeId !== runtimeId && typeof body?.expiresAt === "number" && body.expiresAt > now()
|
|
306
|
+
);
|
|
307
|
+
const workspaceId = existing && !claimedElsewhere ? existing : randomId();
|
|
308
|
+
session.setItem(sessionKey, workspaceId);
|
|
309
|
+
claimLease(workspaceId);
|
|
310
|
+
return workspaceId;
|
|
311
|
+
} catch {
|
|
312
|
+
claimLease(fallbackWorkspaceId);
|
|
313
|
+
return fallbackWorkspaceId;
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
renew(workspaceId) {
|
|
317
|
+
claimLease(workspaceId);
|
|
318
|
+
},
|
|
319
|
+
release(workspaceId) {
|
|
320
|
+
if (leaseBody(workspaceId)?.runtimeId === runtimeId) removeKey(leaseKey(workspaceId));
|
|
321
|
+
if (leased === workspaceId) leased = null;
|
|
322
|
+
},
|
|
323
|
+
index(identityKey, workspaceId) {
|
|
324
|
+
const at = now();
|
|
325
|
+
const prefix = indexPrefix(identityKey);
|
|
326
|
+
const rows = storageKeys(prefix).map((key) => readJson(key)).filter(
|
|
327
|
+
(row) => typeof row === "object" && row !== null && typeof row.workspaceId === "string" && typeof row.lastUsedAt === "number"
|
|
328
|
+
);
|
|
329
|
+
const current = rows.find((row) => row.workspaceId === workspaceId);
|
|
330
|
+
if (current && rows.length <= MAX_WORKSPACES_PER_IDENTITY && at - current.lastUsedAt < INDEX_REFRESH_MS) return;
|
|
331
|
+
const entry = { workspaceId, lastUsedAt: current && at - current.lastUsedAt < INDEX_REFRESH_MS ? current.lastUsedAt : at };
|
|
332
|
+
writeJson(`${prefix}${workspaceId}`, entry);
|
|
333
|
+
const ordered = [...rows.filter((row) => row.workspaceId !== workspaceId), entry].sort((a, b) => b.lastUsedAt - a.lastUsedAt);
|
|
334
|
+
for (const stale of ordered.slice(MAX_WORKSPACES_PER_IDENTITY)) {
|
|
335
|
+
if (hasLiveLease(stale.workspaceId, at)) continue;
|
|
336
|
+
const key = identityScopedKey(identityKey, tabWorkspaceKey(stale.workspaceId));
|
|
337
|
+
void enqueue(key, async () => {
|
|
338
|
+
if (hasLiveLease(stale.workspaceId, now())) return;
|
|
339
|
+
removeKey(mirrorKey(key));
|
|
340
|
+
removeKey(leaseKey(stale.workspaceId));
|
|
341
|
+
removeKey(`${prefix}${stale.workspaceId}`);
|
|
342
|
+
await backend.delete(key);
|
|
343
|
+
}).catch(() => void 0);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
return {
|
|
348
|
+
perPerson: false,
|
|
349
|
+
workspaces,
|
|
350
|
+
async load(key) {
|
|
351
|
+
const mirror = readMirror(key);
|
|
352
|
+
if (mirror) return { value: mirror.value, version: mirror.version };
|
|
353
|
+
const warm = readWarm(key).catch(() => null);
|
|
354
|
+
let timer;
|
|
355
|
+
const timeout = new Promise((resolve) => {
|
|
356
|
+
timer = setTimeout(() => resolve("timeout"), budgetMs);
|
|
357
|
+
});
|
|
358
|
+
const first = await Promise.race([warm, timeout]);
|
|
359
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
360
|
+
if (first === "timeout") throw new PersistenceLoadTimeoutError(key, budgetMs, warm);
|
|
361
|
+
return first;
|
|
362
|
+
},
|
|
363
|
+
async save(key, value, saveOptions) {
|
|
364
|
+
const current = readMirror(key)?.version ?? 0;
|
|
365
|
+
const expected = saveOptions?.expectedVersion;
|
|
366
|
+
if (expected !== void 0 && expected !== current) throw new PersistenceVersionConflictError(key, expected, current);
|
|
367
|
+
const record = { v: RECORD_VERSION, version: current + 1, value };
|
|
368
|
+
if (!writeJson(mirrorKey(key), record)) removeKey(mirrorKey(key));
|
|
369
|
+
await enqueue(key, () => backend.set(key, record));
|
|
370
|
+
return { version: record.version };
|
|
371
|
+
},
|
|
372
|
+
async remove(key) {
|
|
373
|
+
removeKey(mirrorKey(key));
|
|
374
|
+
await enqueue(key, () => backend.delete(key));
|
|
375
|
+
},
|
|
376
|
+
async list(prefix) {
|
|
377
|
+
const fromMirror = storageKeys(mirrorKey(prefix)).map((key) => key.slice(ns.length + 1));
|
|
378
|
+
let fromWarm = [];
|
|
379
|
+
try {
|
|
380
|
+
fromWarm = await backend.keys(prefix);
|
|
381
|
+
} catch {
|
|
382
|
+
}
|
|
383
|
+
return [.../* @__PURE__ */ new Set([...fromMirror, ...fromWarm])].sort();
|
|
384
|
+
},
|
|
385
|
+
async purgeIdentity(identityKey) {
|
|
386
|
+
const prefix = identityScopedKey(identityKey, "");
|
|
387
|
+
for (const key of storageKeys(mirrorKey(prefix))) removeKey(key);
|
|
388
|
+
for (const key of storageKeys(indexPrefix(identityKey))) removeKey(key);
|
|
389
|
+
let warmKeys = [];
|
|
390
|
+
try {
|
|
391
|
+
warmKeys = await backend.keys(prefix);
|
|
392
|
+
} catch {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
await Promise.all(warmKeys.map((key) => enqueue(key, () => backend.delete(key))));
|
|
396
|
+
},
|
|
397
|
+
async idle() {
|
|
398
|
+
await Promise.all([...queues.values()].map((q) => q.catch(() => void 0)));
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// src/persistence/workspace.ts
|
|
404
|
+
var import_summon = require("@ai-matrx/summon");
|
|
405
|
+
var WINDOW_WORKSPACE_SCHEMA_VERSION = 1;
|
|
406
|
+
var DEFAULT_WINDOW_DATA_BYTES = 32 * 1024;
|
|
407
|
+
var MAX_WINDOW_WORKSPACE_BYTES = 256 * 1024;
|
|
408
|
+
var MAX_PERSISTED_WINDOW_SESSIONS = 64;
|
|
409
|
+
var MAX_ID_LENGTH = 160;
|
|
410
|
+
var MAX_LABEL_LENGTH = 512;
|
|
411
|
+
function preservationFor(overlayId) {
|
|
412
|
+
const meta = (0, import_summon.getOverlayMeta)(overlayId);
|
|
413
|
+
const preservation = meta?.window?.preservation;
|
|
414
|
+
if (!meta || !preservation || meta.window?.ephemeral) return null;
|
|
415
|
+
return { meta, preservation };
|
|
416
|
+
}
|
|
417
|
+
function isRecord(value) {
|
|
418
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
419
|
+
}
|
|
420
|
+
var isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
|
|
421
|
+
function utf8Bytes(value) {
|
|
422
|
+
let bytes = 0;
|
|
423
|
+
for (const ch of value) {
|
|
424
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
425
|
+
bytes += cp <= 127 ? 1 : cp <= 2047 ? 2 : cp <= 65535 ? 3 : 4;
|
|
426
|
+
}
|
|
427
|
+
return bytes;
|
|
428
|
+
}
|
|
429
|
+
var isWindowState = (value) => value === "windowed" || value === "maximized" || value === "minimized";
|
|
430
|
+
function readRect(value) {
|
|
431
|
+
if (!isRecord(value)) return null;
|
|
432
|
+
const { x, y, width, height } = value;
|
|
433
|
+
if (!isFiniteNumber(x) || !isFiniteNumber(y) || !isFiniteNumber(width) || !isFiniteNumber(height) || width <= 0 || height <= 0) return null;
|
|
434
|
+
return { x, y, width, height };
|
|
435
|
+
}
|
|
436
|
+
function sanitizeJsonRecord(value, allowedKeys) {
|
|
437
|
+
if (!isRecord(value) || allowedKeys && allowedKeys.length === 0) return { data: {}, droppedUnsupported: false, bytes: 2 };
|
|
438
|
+
const ancestors = /* @__PURE__ */ new WeakSet();
|
|
439
|
+
let droppedUnsupported = false;
|
|
440
|
+
const DROPPED = /* @__PURE__ */ Symbol("dropped");
|
|
441
|
+
const copy = (item) => {
|
|
442
|
+
if (item === void 0 || typeof item === "function" || typeof item === "symbol") {
|
|
443
|
+
droppedUnsupported = true;
|
|
444
|
+
return DROPPED;
|
|
445
|
+
}
|
|
446
|
+
if (typeof item === "bigint") {
|
|
447
|
+
droppedUnsupported = true;
|
|
448
|
+
return item.toString();
|
|
449
|
+
}
|
|
450
|
+
if (typeof item === "number" && !Number.isFinite(item)) {
|
|
451
|
+
droppedUnsupported = true;
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
if (item === null || typeof item !== "object") return item;
|
|
455
|
+
if (item instanceof Date) return item.toJSON();
|
|
456
|
+
if (ancestors.has(item)) {
|
|
457
|
+
droppedUnsupported = true;
|
|
458
|
+
return DROPPED;
|
|
459
|
+
}
|
|
460
|
+
ancestors.add(item);
|
|
461
|
+
let result;
|
|
462
|
+
if (Array.isArray(item)) {
|
|
463
|
+
result = item.map((child) => {
|
|
464
|
+
const copied = copy(child);
|
|
465
|
+
return copied === DROPPED ? null : copied;
|
|
466
|
+
});
|
|
467
|
+
} else if (Object.getPrototypeOf(item) === Object.prototype || Object.getPrototypeOf(item) === null) {
|
|
468
|
+
result = Object.fromEntries(
|
|
469
|
+
Object.entries(item).flatMap(([key, child]) => {
|
|
470
|
+
const copied = copy(child);
|
|
471
|
+
return copied === DROPPED ? [] : [[key, copied]];
|
|
472
|
+
})
|
|
473
|
+
);
|
|
474
|
+
} else {
|
|
475
|
+
droppedUnsupported = true;
|
|
476
|
+
result = DROPPED;
|
|
477
|
+
}
|
|
478
|
+
ancestors.delete(item);
|
|
479
|
+
return result;
|
|
480
|
+
};
|
|
481
|
+
try {
|
|
482
|
+
const allowed = allowedKeys ? new Set(allowedKeys) : null;
|
|
483
|
+
const selected = Object.fromEntries(Object.entries(value).filter(([key]) => !allowed || allowed.has(key)));
|
|
484
|
+
ancestors.add(value);
|
|
485
|
+
const copied = copy(selected);
|
|
486
|
+
ancestors.delete(value);
|
|
487
|
+
const json = JSON.stringify(copied === DROPPED ? {} : copied) || "{}";
|
|
488
|
+
const parsed = JSON.parse(json);
|
|
489
|
+
return { data: isRecord(parsed) ? parsed : {}, droppedUnsupported, bytes: utf8Bytes(json) };
|
|
490
|
+
} catch {
|
|
491
|
+
return { data: {}, droppedUnsupported: true, bytes: 2 };
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
function invalidAllowedDataKey(data, preservation) {
|
|
495
|
+
const domains = preservation.allowedDataValues;
|
|
496
|
+
if (!domains) return null;
|
|
497
|
+
return Object.entries(domains).find(([key, allowed]) => !allowed.includes(data[key]))?.[0] ?? null;
|
|
498
|
+
}
|
|
499
|
+
function sessionFromWindow(entry, savedAt, diagnostics) {
|
|
500
|
+
const persistence = entry.persistence;
|
|
501
|
+
if (!persistence || persistence.closing) return null;
|
|
502
|
+
const preserved = preservationFor(persistence.overlayId);
|
|
503
|
+
if (!preserved) return null;
|
|
504
|
+
const sessionKey = (0, import_summon.windowSessionKey)(persistence.overlayId, persistence.instanceId);
|
|
505
|
+
if (persistence.instanceId.length > MAX_ID_LENGTH || entry.id.length > MAX_ID_LENGTH || entry.title.length > MAX_LABEL_LENGTH) {
|
|
506
|
+
diagnostics.push({ level: "error", code: "invalid-session", sessionKey, message: `Skipped ${sessionKey}: identity or label metadata is oversized.` });
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
const sanitized = sanitizeJsonRecord(persistence.data, preserved.preservation.dataKeys);
|
|
510
|
+
if (sanitized.droppedUnsupported) {
|
|
511
|
+
diagnostics.push({ level: "warning", code: "unsupported-data", sessionKey, message: `Dropped non-JSON window data for ${sessionKey}.` });
|
|
512
|
+
}
|
|
513
|
+
const maxBytes = preserved.preservation.maxDataBytes ?? DEFAULT_WINDOW_DATA_BYTES;
|
|
514
|
+
const data = sanitized.bytes <= maxBytes ? sanitized.data : {};
|
|
515
|
+
if (sanitized.bytes > maxBytes) {
|
|
516
|
+
diagnostics.push({
|
|
517
|
+
level: "error",
|
|
518
|
+
code: "window-data-too-large",
|
|
519
|
+
sessionKey,
|
|
520
|
+
message: `Discarded ${sanitized.bytes} bytes of window data for ${sessionKey}; limit is ${maxBytes}.`
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
const invalidKey = invalidAllowedDataKey(sanitized.data, preserved.preservation);
|
|
524
|
+
if (invalidKey) {
|
|
525
|
+
diagnostics.push({ level: "error", code: "invalid-session", sessionKey, message: `Skipped ${sessionKey}: ${invalidKey} is outside its allowed domain.` });
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
return {
|
|
529
|
+
schemaVersion: import_summon.WINDOW_SESSION_SCHEMA_VERSION,
|
|
530
|
+
sessionKey,
|
|
531
|
+
overlayId: persistence.overlayId,
|
|
532
|
+
instanceId: persistence.instanceId,
|
|
533
|
+
windowId: entry.id,
|
|
534
|
+
title: entry.title,
|
|
535
|
+
state: entry.state,
|
|
536
|
+
windowedRect: entry.state === "minimized" ? entry.preMinimizedRect ?? entry.windowed : entry.windowed,
|
|
537
|
+
traySlot: entry.state === "minimized" ? entry.traySlot : null,
|
|
538
|
+
zIndex: entry.zIndex,
|
|
539
|
+
sidebarOpen: persistence.sidebarOpen,
|
|
540
|
+
sidebarSize: persistence.sidebarSize,
|
|
541
|
+
data,
|
|
542
|
+
savedAt
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
function readSession(value, diagnostics) {
|
|
546
|
+
if (!isRecord(value)) return null;
|
|
547
|
+
const { overlayId, instanceId } = value;
|
|
548
|
+
const rect = readRect(value.windowedRect);
|
|
549
|
+
if (value.schemaVersion !== import_summon.WINDOW_SESSION_SCHEMA_VERSION || typeof overlayId !== "string" || typeof instanceId !== "string" || instanceId.length === 0 || !rect || !isWindowState(value.state)) {
|
|
550
|
+
diagnostics.push({ level: "error", code: "invalid-session", message: "Ignored an invalid window preservation record." });
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
const sessionKey = (0, import_summon.windowSessionKey)(overlayId, instanceId);
|
|
554
|
+
const meta = (0, import_summon.getOverlayMeta)(overlayId);
|
|
555
|
+
if (!meta) {
|
|
556
|
+
diagnostics.push({ level: "error", code: "unknown-overlay", sessionKey, message: `Ignored unknown overlay ${overlayId}.` });
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
const preserved = preservationFor(overlayId);
|
|
560
|
+
if (!preserved) {
|
|
561
|
+
diagnostics.push({ level: "warning", code: "preservation-disabled", sessionKey, message: `Ignored ${overlayId}: its definition does not preserve windows.` });
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
if (meta.instance !== "multi" && instanceId !== "default" || instanceId.length > MAX_ID_LENGTH || typeof value.windowId === "string" && value.windowId.length > MAX_ID_LENGTH || typeof value.title === "string" && value.title.length > MAX_LABEL_LENGTH) {
|
|
565
|
+
diagnostics.push({ level: "error", code: "invalid-session", sessionKey, message: `Ignored invalid instance or oversized metadata for ${overlayId}.` });
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
const sanitized = sanitizeJsonRecord(value.data, preserved.preservation.dataKeys);
|
|
569
|
+
const missing = preserved.preservation.requiredDataKeys?.find((key) => {
|
|
570
|
+
const v = sanitized.data[key];
|
|
571
|
+
return v === void 0 || v === null || v === "";
|
|
572
|
+
});
|
|
573
|
+
if (missing) {
|
|
574
|
+
diagnostics.push({ level: "warning", code: "invalid-session", sessionKey, message: `Ignored ${overlayId}: required key ${missing} was empty.` });
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
const invalidKey = invalidAllowedDataKey(sanitized.data, preserved.preservation);
|
|
578
|
+
if (invalidKey) {
|
|
579
|
+
diagnostics.push({ level: "error", code: "invalid-session", sessionKey, message: `Ignored ${overlayId}: ${invalidKey} is outside its allowed domain.` });
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
const maxBytes = preserved.preservation.maxDataBytes ?? DEFAULT_WINDOW_DATA_BYTES;
|
|
583
|
+
if (sanitized.bytes > maxBytes) {
|
|
584
|
+
diagnostics.push({
|
|
585
|
+
level: "error",
|
|
586
|
+
code: "window-data-too-large",
|
|
587
|
+
sessionKey,
|
|
588
|
+
message: `Discarded ${sanitized.bytes} bytes of restored window data for ${overlayId}; limit is ${maxBytes}.`
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
return {
|
|
592
|
+
schemaVersion: import_summon.WINDOW_SESSION_SCHEMA_VERSION,
|
|
593
|
+
sessionKey,
|
|
594
|
+
overlayId,
|
|
595
|
+
instanceId,
|
|
596
|
+
windowId: typeof value.windowId === "string" ? value.windowId : (0, import_summon.defaultWindowIdFor)(meta),
|
|
597
|
+
title: typeof value.title === "string" ? value.title : meta.label,
|
|
598
|
+
state: value.state,
|
|
599
|
+
windowedRect: rect,
|
|
600
|
+
traySlot: Number.isInteger(value.traySlot) && Number(value.traySlot) >= 0 ? Number(value.traySlot) : null,
|
|
601
|
+
zIndex: isFiniteNumber(value.zIndex) ? value.zIndex : 1e3,
|
|
602
|
+
sidebarOpen: value.sidebarOpen !== false,
|
|
603
|
+
sidebarSize: isFiniteNumber(value.sidebarSize) && value.sidebarSize > 0 ? value.sidebarSize : null,
|
|
604
|
+
data: sanitized.bytes <= maxBytes ? sanitized.data : {},
|
|
605
|
+
savedAt: isFiniteNumber(value.savedAt) ? value.savedAt : 0
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
function serializeWindowWorkspace(state, workspaceId, savedAt = Date.now()) {
|
|
609
|
+
const diagnostics = [];
|
|
610
|
+
const live = Object.values(state.windows).map((entry) => sessionFromWindow(entry, savedAt, diagnostics)).filter((s) => s !== null);
|
|
611
|
+
const liveKeys = new Set(live.map((s) => s.sessionKey));
|
|
612
|
+
const pending = Object.values(state.pendingRestores).filter((s) => !liveKeys.has(s.sessionKey)).map((s) => readSession({ ...s, savedAt }, diagnostics)).filter((s) => s !== null);
|
|
613
|
+
const all = [...live, ...pending].sort((a, b) => b.zIndex - a.zIndex || a.sessionKey.localeCompare(b.sessionKey));
|
|
614
|
+
const sessions = all.slice(0, MAX_PERSISTED_WINDOW_SESSIONS).sort((a, b) => a.zIndex - b.zIndex || a.sessionKey.localeCompare(b.sessionKey));
|
|
615
|
+
if (all.length > sessions.length) {
|
|
616
|
+
diagnostics.push({
|
|
617
|
+
level: "error",
|
|
618
|
+
code: "workspace-too-large",
|
|
619
|
+
message: `Window workspace exceeded ${MAX_PERSISTED_WINDOW_SESSIONS} sessions; later z-order entries were not saved.`
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
const workspace = { schemaVersion: WINDOW_WORKSPACE_SCHEMA_VERSION, workspaceId, savedAt, sessions };
|
|
623
|
+
const bytes = utf8Bytes(JSON.stringify(workspace));
|
|
624
|
+
if (bytes > MAX_WINDOW_WORKSPACE_BYTES) {
|
|
625
|
+
for (const s of sessions) s.data = {};
|
|
626
|
+
diagnostics.push({
|
|
627
|
+
level: "error",
|
|
628
|
+
code: "workspace-too-large",
|
|
629
|
+
message: `Window workspace reached ${bytes} bytes; window data was discarded while geometry was kept.`
|
|
630
|
+
});
|
|
631
|
+
while (sessions.length > 0 && utf8Bytes(JSON.stringify(workspace)) > MAX_WINDOW_WORKSPACE_BYTES) sessions.pop();
|
|
632
|
+
}
|
|
633
|
+
return { workspace, diagnostics };
|
|
634
|
+
}
|
|
635
|
+
function fallbackRect(viewport) {
|
|
636
|
+
return (0, import_summon.centerRectInViewport)({ x: 0, y: 0, width: Math.min(720, viewport.width), height: Math.min(480, viewport.height) }, viewport);
|
|
637
|
+
}
|
|
638
|
+
function hydrateWindowWorkspace(raw, viewport, expectedWorkspaceId) {
|
|
639
|
+
const diagnostics = [];
|
|
640
|
+
if (!isRecord(raw) || raw.schemaVersion !== WINDOW_WORKSPACE_SCHEMA_VERSION || !Array.isArray(raw.sessions) || typeof raw.workspaceId !== "string" || expectedWorkspaceId !== void 0 && raw.workspaceId !== expectedWorkspaceId) {
|
|
641
|
+
if (raw != null) diagnostics.push({ level: "error", code: "invalid-workspace", message: "Ignored an invalid or unsupported window workspace." });
|
|
642
|
+
return { sessions: [], diagnostics };
|
|
643
|
+
}
|
|
644
|
+
let workspaceBytes;
|
|
645
|
+
try {
|
|
646
|
+
workspaceBytes = utf8Bytes(JSON.stringify(raw));
|
|
647
|
+
} catch {
|
|
648
|
+
diagnostics.push({ level: "error", code: "invalid-workspace", message: "Ignored a window workspace that could not be serialized." });
|
|
649
|
+
return { sessions: [], diagnostics };
|
|
650
|
+
}
|
|
651
|
+
if (workspaceBytes > MAX_WINDOW_WORKSPACE_BYTES) {
|
|
652
|
+
diagnostics.push({
|
|
653
|
+
level: "error",
|
|
654
|
+
code: "workspace-too-large",
|
|
655
|
+
message: `Ignored a ${workspaceBytes}-byte window workspace above the ${MAX_WINDOW_WORKSPACE_BYTES}-byte limit.`
|
|
656
|
+
});
|
|
657
|
+
return { sessions: [], diagnostics };
|
|
658
|
+
}
|
|
659
|
+
if (raw.sessions.length > MAX_PERSISTED_WINDOW_SESSIONS) {
|
|
660
|
+
diagnostics.push({
|
|
661
|
+
level: "error",
|
|
662
|
+
code: "invalid-workspace",
|
|
663
|
+
message: `Window workspace exceeded ${MAX_PERSISTED_WINDOW_SESSIONS} sessions; extras were ignored.`
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
667
|
+
for (const candidate of raw.sessions.slice(0, MAX_PERSISTED_WINDOW_SESSIONS)) {
|
|
668
|
+
const session = readSession(candidate, diagnostics);
|
|
669
|
+
if (!session) continue;
|
|
670
|
+
const existing = byKey.get(session.sessionKey);
|
|
671
|
+
if (!existing || existing.savedAt < session.savedAt || existing.savedAt === session.savedAt && JSON.stringify(existing) < JSON.stringify(session)) {
|
|
672
|
+
byKey.set(session.sessionKey, session);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
const ordered = [...byKey.values()].sort((a, b) => a.zIndex - b.zIndex || a.sessionKey.localeCompare(b.sessionKey));
|
|
676
|
+
const minimized = ordered.filter((s) => s.state === "minimized").sort((a, b) => (a.traySlot ?? Number.MAX_SAFE_INTEGER) - (b.traySlot ?? Number.MAX_SAFE_INTEGER) || a.sessionKey.localeCompare(b.sessionKey));
|
|
677
|
+
const slotOf = new Map(minimized.map((s, i) => [s.sessionKey, i]));
|
|
678
|
+
const sessions = ordered.map((session, index) => {
|
|
679
|
+
const restoreDefaults = (0, import_summon.getOverlayMeta)(session.overlayId)?.window?.restoreDefaults;
|
|
680
|
+
const slot = slotOf.get(session.sessionKey) ?? null;
|
|
681
|
+
const renderRect = session.state === "minimized" && slot !== null ? (0, import_summon.traySlotRect)(slot, viewport.width, viewport.height, minimized.length) : session.state === "windowed" || session.state === "maximized" ? (0, import_summon.clampRectToViewport)(session.windowedRect, viewport) : fallbackRect(viewport);
|
|
682
|
+
return {
|
|
683
|
+
...session,
|
|
684
|
+
traySlot: slot,
|
|
685
|
+
zIndex: 1e3 + index,
|
|
686
|
+
data: restoreDefaults ? { ...restoreDefaults, ...session.data } : session.data,
|
|
687
|
+
renderRect
|
|
688
|
+
};
|
|
689
|
+
});
|
|
690
|
+
return { sessions, diagnostics };
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// src/persistence/controller.ts
|
|
694
|
+
var import_summon2 = require("@ai-matrx/summon");
|
|
695
|
+
var DEFAULT_SAVE_DEBOUNCE_MS = 250;
|
|
696
|
+
var STALE_SWEEP_MS = 30 * 60 * 1e3;
|
|
697
|
+
function isRecord2(value) {
|
|
698
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
699
|
+
}
|
|
700
|
+
function openRegisteredOverlay(dispatch, overlayId, instanceId, data) {
|
|
701
|
+
const defaults = (0, import_summon2.getOverlayMeta)(overlayId)?.defaults;
|
|
702
|
+
dispatch(import_summon2.overlaysRawActions.openOverlay({ overlayId, instanceId, data, ...isRecord2(defaults) ? { defaults } : {} }));
|
|
703
|
+
}
|
|
704
|
+
function persistsComponentState(overlayId) {
|
|
705
|
+
const meta = (0, import_summon2.getOverlayMeta)(overlayId);
|
|
706
|
+
return Boolean(meta && (meta.keepAlive === "restore" || preservationFor(overlayId)));
|
|
707
|
+
}
|
|
708
|
+
function createOverlayPersistenceController(store, ports, options = {}) {
|
|
709
|
+
const port = ports.persistence;
|
|
710
|
+
const debounceMs = options.saveDebounceMs ?? DEFAULT_SAVE_DEBOUNCE_MS;
|
|
711
|
+
const seedFromPerson = options.seedFromPerson ?? true;
|
|
712
|
+
const local = isLocalPersistence(port) ? port : null;
|
|
713
|
+
const scope = options.scope ?? (port?.perPerson ? "person" : local ? "tab" : "person");
|
|
714
|
+
let identityKey = null;
|
|
715
|
+
let generation = 0;
|
|
716
|
+
let writeSafe = false;
|
|
717
|
+
let hydrated = false;
|
|
718
|
+
let workspaceId = null;
|
|
719
|
+
let saveTimer = null;
|
|
720
|
+
let lastWorkspaceFp = null;
|
|
721
|
+
let lastSavedAt = 0;
|
|
722
|
+
const versions = /* @__PURE__ */ new Map();
|
|
723
|
+
const stateFps = /* @__PURE__ */ new Map();
|
|
724
|
+
const chains = /* @__PURE__ */ new Map();
|
|
725
|
+
const closedExact = /* @__PURE__ */ new Set();
|
|
726
|
+
const closedOverlay = /* @__PURE__ */ new Set();
|
|
727
|
+
let closedAll = false;
|
|
728
|
+
const cleanups = [];
|
|
729
|
+
const setHydrated = (next) => {
|
|
730
|
+
if (hydrated === next) return;
|
|
731
|
+
hydrated = next;
|
|
732
|
+
options.onHydratedChange?.(next);
|
|
733
|
+
};
|
|
734
|
+
const report = (diagnostics) => {
|
|
735
|
+
for (const d of diagnostics) {
|
|
736
|
+
ports.errors.capture(new Error(`[overlays/persistence:${d.code}] ${d.message}`), {
|
|
737
|
+
source: "overlay-persistence",
|
|
738
|
+
level: d.level,
|
|
739
|
+
code: d.code,
|
|
740
|
+
...d.sessionKey ? { sessionKey: d.sessionKey } : {}
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
const fail = (where) => (error) => ports.errors.capture(error, { source: "overlay-persistence", where });
|
|
745
|
+
const scoped = (key) => port?.perPerson || identityKey === null ? key : identityScopedKey(identityKey, key);
|
|
746
|
+
const workspaceKey = () => scope === "tab" && workspaceId ? tabWorkspaceKey(workspaceId) : PERSON_WORKSPACE_KEY;
|
|
747
|
+
const put = (key, value) => {
|
|
748
|
+
if (!port) return;
|
|
749
|
+
const gen = generation;
|
|
750
|
+
const run = async () => {
|
|
751
|
+
if (gen !== generation) return;
|
|
752
|
+
const expected = versions.get(key);
|
|
753
|
+
try {
|
|
754
|
+
const saved = await port.save(key, value, expected === void 0 ? void 0 : { expectedVersion: expected });
|
|
755
|
+
versions.set(key, saved.version);
|
|
756
|
+
} catch (error) {
|
|
757
|
+
if (!isVersionConflict(error) || gen !== generation) throw error;
|
|
758
|
+
const current = await port.load(key).catch(() => null);
|
|
759
|
+
const saved = await port.save(key, value, { expectedVersion: current?.version ?? 0 });
|
|
760
|
+
versions.set(key, saved.version);
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
const next = (chains.get(key) ?? Promise.resolve()).then(run).catch(fail(`save ${key}`));
|
|
764
|
+
chains.set(key, next);
|
|
765
|
+
};
|
|
766
|
+
const drop = (key) => {
|
|
767
|
+
if (!port) return;
|
|
768
|
+
const next = (chains.get(key) ?? Promise.resolve()).then(() => port.remove(key)).then(() => void versions.delete(key)).catch(fail(`remove ${key}`));
|
|
769
|
+
chains.set(key, next);
|
|
770
|
+
};
|
|
771
|
+
const saveStates = (state) => {
|
|
772
|
+
const present = /* @__PURE__ */ new Set();
|
|
773
|
+
for (const [key, entry] of Object.entries(state.overlayState.entries)) {
|
|
774
|
+
const parsed = (0, import_summon2.parseInstanceKey)(key);
|
|
775
|
+
if (!parsed || !persistsComponentState(parsed.overlayId)) continue;
|
|
776
|
+
const sanitized = sanitizeJsonRecord(entry.values, null);
|
|
777
|
+
if (sanitized.bytes > DEFAULT_WINDOW_DATA_BYTES) {
|
|
778
|
+
report([
|
|
779
|
+
{
|
|
780
|
+
level: "error",
|
|
781
|
+
code: "state-too-large",
|
|
782
|
+
sessionKey: key,
|
|
783
|
+
message: `Saved component state of ${key} is ${sanitized.bytes} bytes; limit is ${DEFAULT_WINDOW_DATA_BYTES}. Not saved.`
|
|
784
|
+
}
|
|
785
|
+
]);
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
const storageKey = scoped(stateKey(key));
|
|
789
|
+
present.add(storageKey);
|
|
790
|
+
const fp = JSON.stringify(sanitized.data);
|
|
791
|
+
if (stateFps.get(storageKey) === fp) continue;
|
|
792
|
+
stateFps.set(storageKey, fp);
|
|
793
|
+
put(storageKey, sanitized.data);
|
|
794
|
+
}
|
|
795
|
+
for (const storageKey of [...stateFps.keys()]) {
|
|
796
|
+
if (present.has(storageKey)) continue;
|
|
797
|
+
stateFps.delete(storageKey);
|
|
798
|
+
drop(storageKey);
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
const saveNow = () => {
|
|
802
|
+
if (saveTimer) {
|
|
803
|
+
clearTimeout(saveTimer);
|
|
804
|
+
saveTimer = null;
|
|
805
|
+
}
|
|
806
|
+
if (!port || !writeSafe || identityKey === null) return;
|
|
807
|
+
const state = store.getState();
|
|
808
|
+
const savedAt = Math.max(Date.now(), lastSavedAt + 1);
|
|
809
|
+
const { workspace, diagnostics } = serializeWindowWorkspace(state.windowManager, workspaceId ?? PERSON_WORKSPACE_KEY, savedAt);
|
|
810
|
+
report(diagnostics);
|
|
811
|
+
const fp = JSON.stringify(workspace.sessions.map((s) => ({ ...s, savedAt: 0 })));
|
|
812
|
+
if (fp !== lastWorkspaceFp) {
|
|
813
|
+
lastWorkspaceFp = fp;
|
|
814
|
+
lastSavedAt = savedAt;
|
|
815
|
+
put(scoped(workspaceKey()), workspace);
|
|
816
|
+
if (scope === "tab" && workspaceId) {
|
|
817
|
+
if (seedFromPerson) put(scoped(PERSON_WORKSPACE_KEY), workspace);
|
|
818
|
+
local?.workspaces.index(identityKey, workspaceId);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
saveStates(state);
|
|
822
|
+
};
|
|
823
|
+
const scheduleSave = () => {
|
|
824
|
+
if (saveTimer) clearTimeout(saveTimer);
|
|
825
|
+
saveTimer = setTimeout(() => {
|
|
826
|
+
saveTimer = null;
|
|
827
|
+
saveNow();
|
|
828
|
+
}, debounceMs);
|
|
829
|
+
};
|
|
830
|
+
const loadValue = async (key) => {
|
|
831
|
+
if (!port) return null;
|
|
832
|
+
try {
|
|
833
|
+
return await port.load(key);
|
|
834
|
+
} catch (error) {
|
|
835
|
+
if (isLoadTimeout(error)) return error.pending;
|
|
836
|
+
throw error;
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
const teardown = () => {
|
|
840
|
+
generation += 1;
|
|
841
|
+
writeSafe = false;
|
|
842
|
+
setHydrated(false);
|
|
843
|
+
if (saveTimer) {
|
|
844
|
+
clearTimeout(saveTimer);
|
|
845
|
+
saveTimer = null;
|
|
846
|
+
}
|
|
847
|
+
const state = store.getState();
|
|
848
|
+
const identities = /* @__PURE__ */ new Map();
|
|
849
|
+
for (const entry of Object.values(state.windowManager.windows)) {
|
|
850
|
+
if (entry.persistence) {
|
|
851
|
+
const { overlayId, instanceId } = entry.persistence;
|
|
852
|
+
identities.set((0, import_summon2.windowSessionKey)(overlayId, instanceId), { overlayId, instanceId });
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
for (const session of Object.values(state.windowManager.pendingRestores)) {
|
|
856
|
+
identities.set(session.sessionKey, { overlayId: session.overlayId, instanceId: session.instanceId });
|
|
857
|
+
}
|
|
858
|
+
for (const identity of identities.values()) store.dispatch((0, import_summon2.markWindowClosing)(identity));
|
|
859
|
+
store.dispatch((0, import_summon2.closeAllOverlays)());
|
|
860
|
+
store.dispatch((0, import_summon2.clearWindowPersistenceState)());
|
|
861
|
+
for (const key of Object.keys(store.getState().overlayState.entries)) store.dispatch((0, import_summon2.clearOverlayState)({ key }));
|
|
862
|
+
closedExact.clear();
|
|
863
|
+
closedOverlay.clear();
|
|
864
|
+
closedAll = false;
|
|
865
|
+
versions.clear();
|
|
866
|
+
stateFps.clear();
|
|
867
|
+
lastWorkspaceFp = null;
|
|
868
|
+
};
|
|
869
|
+
const liveWindowFor = (windows, overlayId, instanceId) => Object.values(windows).find(
|
|
870
|
+
(entry) => entry.persistence?.overlayId === overlayId && entry.persistence.instanceId === instanceId && !entry.persistence.closing
|
|
871
|
+
);
|
|
872
|
+
const applyGeometry = (entry, session, viewport) => {
|
|
873
|
+
if (entry.state !== "windowed") store.dispatch((0, import_summon2.restoreWindow)(entry.id));
|
|
874
|
+
store.dispatch((0, import_summon2.updateWindowRect)({ id: entry.id, rect: session.state === "minimized" ? session.windowedRect : session.renderRect }));
|
|
875
|
+
if (session.state === "maximized") store.dispatch((0, import_summon2.maximizeWindow)(entry.id));
|
|
876
|
+
else if (session.state === "minimized") {
|
|
877
|
+
store.dispatch((0, import_summon2.minimizeWindow)({ id: entry.id, viewportWidth: viewport.width, viewportHeight: viewport.height }));
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
const loadIdentity = async (key) => {
|
|
881
|
+
if (!port) return;
|
|
882
|
+
const gen = generation;
|
|
883
|
+
if (scope === "tab" && local) workspaceId ??= local.workspaces.currentId();
|
|
884
|
+
else workspaceId = null;
|
|
885
|
+
const ownKey = scoped(workspaceKey());
|
|
886
|
+
let stored = await loadValue(ownKey);
|
|
887
|
+
let expectedWorkspaceId = scope === "tab" && workspaceId ? workspaceId : void 0;
|
|
888
|
+
if (stored) versions.set(ownKey, stored.version);
|
|
889
|
+
if (!stored && scope === "tab" && seedFromPerson) {
|
|
890
|
+
const personKey = scoped(PERSON_WORKSPACE_KEY);
|
|
891
|
+
stored = await loadValue(personKey);
|
|
892
|
+
if (stored) versions.set(personKey, stored.version);
|
|
893
|
+
expectedWorkspaceId = void 0;
|
|
894
|
+
}
|
|
895
|
+
const stateKeys = port.list ? await port.list(scoped(STATE_KEY_PREFIX)) : null;
|
|
896
|
+
if (gen !== generation || identityKey !== key) return;
|
|
897
|
+
const viewport = options.viewport?.() ?? (() => {
|
|
898
|
+
const { vw, vh } = (0, import_summon2.safeViewportDims)();
|
|
899
|
+
return { width: vw, height: vh };
|
|
900
|
+
})();
|
|
901
|
+
const result = hydrateWindowWorkspace(stored?.value ?? null, viewport, expectedWorkspaceId);
|
|
902
|
+
report(result.diagnostics);
|
|
903
|
+
const sessions = result.sessions.filter(
|
|
904
|
+
(s) => !closedAll && !closedOverlay.has(s.overlayId) && !closedExact.has(s.sessionKey)
|
|
905
|
+
);
|
|
906
|
+
const prefix = scoped(STATE_KEY_PREFIX);
|
|
907
|
+
const candidates = stateKeys ? stateKeys.filter((k) => k.startsWith(prefix)).map((k) => k.slice(prefix.length)) : sessions.map((s) => s.sessionKey);
|
|
908
|
+
const loadedStates = await Promise.all(
|
|
909
|
+
candidates.map(async (instance) => [instance, await loadValue(scoped(stateKey(instance))).catch(() => null)])
|
|
910
|
+
);
|
|
911
|
+
if (gen !== generation || identityKey !== key) return;
|
|
912
|
+
const before = store.getState();
|
|
913
|
+
const restoredKeys = new Set(sessions.map((s) => s.sessionKey));
|
|
914
|
+
const stateEntries = {};
|
|
915
|
+
for (const [instance, value] of loadedStates) {
|
|
916
|
+
const parsed = (0, import_summon2.parseInstanceKey)(instance);
|
|
917
|
+
if (!parsed || !value || !isRecord2(value.value)) continue;
|
|
918
|
+
const storageKey = scoped(stateKey(instance));
|
|
919
|
+
const meta = (0, import_summon2.getOverlayMeta)(parsed.overlayId);
|
|
920
|
+
if (!meta || !persistsComponentState(parsed.overlayId)) continue;
|
|
921
|
+
const open = before.overlays.overlays[parsed.overlayId]?.[parsed.instanceId]?.isOpen === true;
|
|
922
|
+
const wanted = meta.keepAlive === "restore" || restoredKeys.has(instance) || open;
|
|
923
|
+
if (!wanted) {
|
|
924
|
+
drop(storageKey);
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
versions.set(storageKey, value.version);
|
|
928
|
+
if (before.overlayState.entries[instance]) continue;
|
|
929
|
+
stateEntries[instance] = { values: value.value };
|
|
930
|
+
stateFps.set(storageKey, JSON.stringify(value.value));
|
|
931
|
+
}
|
|
932
|
+
if (Object.keys(stateEntries).length > 0) store.dispatch((0, import_summon2.hydrateOverlayState)({ entries: stateEntries }));
|
|
933
|
+
const live = [];
|
|
934
|
+
const staged = [];
|
|
935
|
+
for (const session of sessions) {
|
|
936
|
+
const entry = liveWindowFor(before.windowManager.windows, session.overlayId, session.instanceId);
|
|
937
|
+
if (entry) live.push([entry, session]);
|
|
938
|
+
else staged.push(session);
|
|
939
|
+
}
|
|
940
|
+
store.dispatch((0, import_summon2.hydrateWindowSessions)({ sessions: staged, viewportWidth: viewport.width, viewportHeight: viewport.height }));
|
|
941
|
+
for (const [entry, session] of live) applyGeometry(entry, session, viewport);
|
|
942
|
+
for (const session of staged) {
|
|
943
|
+
if (before.overlays.overlays[session.overlayId]?.[session.instanceId]?.isOpen) continue;
|
|
944
|
+
openRegisteredOverlay(store.dispatch, session.overlayId, session.instanceId, session.data);
|
|
945
|
+
}
|
|
946
|
+
lastWorkspaceFp = stored && expectedWorkspaceId !== void 0 && isRecord2(stored.value) && Array.isArray(stored.value.sessions) ? JSON.stringify(stored.value.sessions.map((s) => ({ ...s, savedAt: 0 }))) : null;
|
|
947
|
+
lastSavedAt = stored && isRecord2(stored.value) && typeof stored.value.savedAt === "number" ? stored.value.savedAt : 0;
|
|
948
|
+
writeSafe = true;
|
|
949
|
+
setHydrated(true);
|
|
950
|
+
setTimeout(() => {
|
|
951
|
+
if (gen !== generation) return;
|
|
952
|
+
saveNow();
|
|
953
|
+
closedExact.clear();
|
|
954
|
+
closedOverlay.clear();
|
|
955
|
+
closedAll = false;
|
|
956
|
+
}, 0);
|
|
957
|
+
};
|
|
958
|
+
const onIdentity = () => {
|
|
959
|
+
const current = ports.identity.current();
|
|
960
|
+
if (!current.ready) {
|
|
961
|
+
if (identityKey !== null) teardown();
|
|
962
|
+
identityKey = null;
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
if (current.key === identityKey) return;
|
|
966
|
+
const changed = identityKey !== null;
|
|
967
|
+
if (changed) teardown();
|
|
968
|
+
else generation += 1;
|
|
969
|
+
identityKey = current.key;
|
|
970
|
+
writeSafe = false;
|
|
971
|
+
setHydrated(false);
|
|
972
|
+
const key = current.key;
|
|
973
|
+
const wait = changed ? new Promise((resolve) => setTimeout(resolve, 0)) : Promise.resolve();
|
|
974
|
+
void wait.then(() => loadIdentity(key)).catch((error) => {
|
|
975
|
+
fail("load")(error);
|
|
976
|
+
if (identityKey === key) setHydrated(true);
|
|
977
|
+
});
|
|
978
|
+
};
|
|
979
|
+
return {
|
|
980
|
+
get hydrated() {
|
|
981
|
+
return hydrated;
|
|
982
|
+
},
|
|
983
|
+
flush: saveNow,
|
|
984
|
+
start() {
|
|
985
|
+
if (!port) return;
|
|
986
|
+
cleanups.push(ports.identity.subscribe(onIdentity));
|
|
987
|
+
cleanups.push(
|
|
988
|
+
(0, import_summon2.registerCloseFlusher)((event) => {
|
|
989
|
+
if (writeSafe) {
|
|
990
|
+
saveNow();
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
for (const intent of event.intents) {
|
|
994
|
+
if (intent.scope === "all") closedAll = true;
|
|
995
|
+
else if (intent.scope === "overlay") closedOverlay.add(intent.overlayId);
|
|
996
|
+
else closedExact.add((0, import_summon2.windowSessionKey)(intent.overlayId, intent.instanceId));
|
|
997
|
+
}
|
|
998
|
+
})
|
|
999
|
+
);
|
|
1000
|
+
let previous = store.getState();
|
|
1001
|
+
cleanups.push(
|
|
1002
|
+
store.subscribe(() => {
|
|
1003
|
+
const next = store.getState();
|
|
1004
|
+
const changed = next.windowManager !== previous.windowManager || next.overlayState !== previous.overlayState;
|
|
1005
|
+
previous = next;
|
|
1006
|
+
if (changed && writeSafe) scheduleSave();
|
|
1007
|
+
})
|
|
1008
|
+
);
|
|
1009
|
+
if (typeof window !== "undefined") {
|
|
1010
|
+
const renew = () => {
|
|
1011
|
+
if (workspaceId) local?.workspaces.renew(workspaceId);
|
|
1012
|
+
};
|
|
1013
|
+
const onPageHide = () => {
|
|
1014
|
+
saveNow();
|
|
1015
|
+
if (workspaceId) local?.workspaces.release(workspaceId);
|
|
1016
|
+
};
|
|
1017
|
+
const onVisibility = () => {
|
|
1018
|
+
if (document.visibilityState === "hidden") saveNow();
|
|
1019
|
+
else renew();
|
|
1020
|
+
};
|
|
1021
|
+
window.addEventListener("pagehide", onPageHide);
|
|
1022
|
+
window.addEventListener("pageshow", renew);
|
|
1023
|
+
window.addEventListener("focus", renew);
|
|
1024
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
1025
|
+
const sweep = window.setInterval(() => store.dispatch((0, import_summon2.pruneStaleInstances)({ olderThanMs: STALE_SWEEP_MS })), STALE_SWEEP_MS);
|
|
1026
|
+
cleanups.push(() => {
|
|
1027
|
+
window.removeEventListener("pagehide", onPageHide);
|
|
1028
|
+
window.removeEventListener("pageshow", renew);
|
|
1029
|
+
window.removeEventListener("focus", renew);
|
|
1030
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
1031
|
+
window.clearInterval(sweep);
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
onIdentity();
|
|
1035
|
+
},
|
|
1036
|
+
stop() {
|
|
1037
|
+
saveNow();
|
|
1038
|
+
generation += 1;
|
|
1039
|
+
writeSafe = false;
|
|
1040
|
+
if (saveTimer) clearTimeout(saveTimer);
|
|
1041
|
+
saveTimer = null;
|
|
1042
|
+
for (const cleanup of cleanups.splice(0)) cleanup();
|
|
1043
|
+
identityKey = null;
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// src/persistence/OverlayPersistence.tsx
|
|
1049
|
+
var import_react = require("react");
|
|
1050
|
+
var import_react2 = require("@ai-matrx/summon/react");
|
|
1051
|
+
function useOverlayPersistence(options = {}) {
|
|
1052
|
+
const store = (0, import_react2.useSummonStore)();
|
|
1053
|
+
const ports = (0, import_react2.useSummonPorts)();
|
|
1054
|
+
const [hydrated, setHydrated] = (0, import_react.useState)(false);
|
|
1055
|
+
const { scope, seedFromPerson, saveDebounceMs, viewport } = options;
|
|
1056
|
+
(0, import_react.useEffect)(() => {
|
|
1057
|
+
const controller = createOverlayPersistenceController(store, ports, {
|
|
1058
|
+
...scope === void 0 ? {} : { scope },
|
|
1059
|
+
...seedFromPerson === void 0 ? {} : { seedFromPerson },
|
|
1060
|
+
...saveDebounceMs === void 0 ? {} : { saveDebounceMs },
|
|
1061
|
+
...viewport === void 0 ? {} : { viewport },
|
|
1062
|
+
onHydratedChange: setHydrated
|
|
1063
|
+
});
|
|
1064
|
+
controller.start();
|
|
1065
|
+
return () => controller.stop();
|
|
1066
|
+
}, [store, ports, scope, seedFromPerson, saveDebounceMs, viewport]);
|
|
1067
|
+
return { hydrated };
|
|
1068
|
+
}
|
|
1069
|
+
function OverlayPersistence(props) {
|
|
1070
|
+
useOverlayPersistence(props);
|
|
1071
|
+
return null;
|
|
1072
|
+
}
|
|
1073
|
+
//# sourceMappingURL=persistence.cjs.map
|