latticesql 3.4.0 → 3.4.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.
- package/dist/cli.js +375 -79
- package/dist/index.cjs +372 -78
- package/dist/index.d.cts +28 -1
- package/dist/index.d.ts +28 -1
- package/dist/index.js +369 -75
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -919,43 +919,57 @@ var init_postgres = __esm({
|
|
|
919
919
|
},
|
|
920
920
|
{
|
|
921
921
|
warn: "could not register json_extract polyfill:",
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
922
|
+
// Create ONLY if absent. `CREATE OR REPLACE` on an existing function requires
|
|
923
|
+
// ownership, but on a cloud the function is owned by whichever single role
|
|
924
|
+
// created it first — so every OTHER member's per-connect replace raised "must
|
|
925
|
+
// be owner of function" and (sharing the render transaction) aborted it,
|
|
926
|
+
// yielding an empty render. The IF-absent guard makes a present function a
|
|
927
|
+
// clean no-op for everyone, regardless of who owns it.
|
|
928
|
+
sql: `DO $do$ BEGIN
|
|
929
|
+
IF to_regprocedure('json_extract(text, text)') IS NULL THEN
|
|
930
|
+
CREATE FUNCTION json_extract(doc text, path text)
|
|
931
|
+
RETURNS text
|
|
932
|
+
LANGUAGE sql
|
|
933
|
+
IMMUTABLE
|
|
934
|
+
AS $fn$
|
|
935
|
+
SELECT doc::jsonb #>> string_to_array(regexp_replace(path, '^\\$\\.?', ''), '.')
|
|
936
|
+
$fn$;
|
|
937
|
+
END IF;
|
|
938
|
+
END $do$;`
|
|
929
939
|
},
|
|
930
940
|
{
|
|
931
941
|
warn: "could not register strftime polyfill:",
|
|
932
|
-
sql: `
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
942
|
+
sql: `DO $do$ BEGIN
|
|
943
|
+
IF to_regprocedure('strftime(text, text)') IS NULL THEN
|
|
944
|
+
CREATE FUNCTION strftime(format text, modifier text)
|
|
945
|
+
RETURNS text
|
|
946
|
+
LANGUAGE plpgsql
|
|
947
|
+
IMMUTABLE
|
|
948
|
+
AS $fn$
|
|
949
|
+
DECLARE ts timestamptz;
|
|
950
|
+
BEGIN
|
|
951
|
+
IF modifier = 'now' THEN
|
|
952
|
+
ts := now();
|
|
953
|
+
ELSE
|
|
954
|
+
ts := modifier::timestamptz;
|
|
955
|
+
END IF;
|
|
956
|
+
RETURN to_char(
|
|
957
|
+
ts AT TIME ZONE 'UTC',
|
|
958
|
+
replace(replace(replace(replace(replace(replace(replace(replace(
|
|
959
|
+
format,
|
|
960
|
+
'%Y', 'YYYY'),
|
|
961
|
+
'%m', 'MM'),
|
|
962
|
+
'%d', 'DD'),
|
|
963
|
+
'%H', 'HH24'),
|
|
964
|
+
'%M', 'MI'),
|
|
965
|
+
'%S', 'SS'),
|
|
966
|
+
'%f', 'MS'),
|
|
967
|
+
'T', '"T"')
|
|
968
|
+
);
|
|
969
|
+
END;
|
|
970
|
+
$fn$;
|
|
971
|
+
END IF;
|
|
972
|
+
END $do$;`
|
|
959
973
|
}
|
|
960
974
|
];
|
|
961
975
|
}
|
|
@@ -2364,6 +2378,34 @@ var init_engine = __esm({
|
|
|
2364
2378
|
setRenderFold(fn) {
|
|
2365
2379
|
this._foldRows = fn;
|
|
2366
2380
|
}
|
|
2381
|
+
/**
|
|
2382
|
+
* Incremental scope: is this entity-context table affected by a change to one
|
|
2383
|
+
* of `changed`? Affected when the table itself changed (its own rows / `self`
|
|
2384
|
+
* source / index) OR any of its files SOURCES from a changed table (a cross-
|
|
2385
|
+
* table dependent — e.g. an AGENT.md that lists the agent's tasks must re-render
|
|
2386
|
+
* when `tasks` changes). A `custom` source runs an arbitrary query, so we can't
|
|
2387
|
+
* prove independence — treat it as always-affected (conservative, never stale).
|
|
2388
|
+
*/
|
|
2389
|
+
_entityAffected(table, def, changed) {
|
|
2390
|
+
if (changed.has(table)) return true;
|
|
2391
|
+
for (const spec of Object.values(def.files)) {
|
|
2392
|
+
if (this._sourceTouches(spec.source, changed)) return true;
|
|
2393
|
+
}
|
|
2394
|
+
return false;
|
|
2395
|
+
}
|
|
2396
|
+
_sourceTouches(source, changed) {
|
|
2397
|
+
if (source == null || typeof source !== "object") return false;
|
|
2398
|
+
const s2 = source;
|
|
2399
|
+
if (s2.type === "custom") return true;
|
|
2400
|
+
if (typeof s2.table === "string" && changed.has(s2.table)) return true;
|
|
2401
|
+
if (typeof s2.junctionTable === "string" && changed.has(s2.junctionTable)) return true;
|
|
2402
|
+
if (s2.sources != null && typeof s2.sources === "object") {
|
|
2403
|
+
for (const sub of Object.values(s2.sources)) {
|
|
2404
|
+
if (this._sourceTouches(sub, changed)) return true;
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
return false;
|
|
2408
|
+
}
|
|
2367
2409
|
async render(outputDir, opts = {}) {
|
|
2368
2410
|
const start = Date.now();
|
|
2369
2411
|
const filesWritten = [];
|
|
@@ -2373,6 +2415,7 @@ var init_engine = __esm({
|
|
|
2373
2415
|
for (const [name, def] of this._schema.getTables()) {
|
|
2374
2416
|
if (signal?.aborted) return this._abortedResult(filesWritten, counters, start);
|
|
2375
2417
|
if (this._skipEmpty && def.render === NOOP_RENDER) continue;
|
|
2418
|
+
if (opts.changedTables && !opts.changedTables.has(name)) continue;
|
|
2376
2419
|
let rows = await this._schema.queryTable(this._adapter, name, this._readRel);
|
|
2377
2420
|
if (def.relevanceFilter) {
|
|
2378
2421
|
const ctx = this._getTaskContext();
|
|
@@ -2425,6 +2468,9 @@ var init_engine = __esm({
|
|
|
2425
2468
|
}
|
|
2426
2469
|
for (const [name, def] of this._schema.getMultis()) {
|
|
2427
2470
|
if (signal?.aborted) return this._abortedResult(filesWritten, counters, start);
|
|
2471
|
+
if (opts.changedTables && def.tables && !def.tables.some((t8) => opts.changedTables?.has(t8))) {
|
|
2472
|
+
continue;
|
|
2473
|
+
}
|
|
2428
2474
|
const keys = await def.keys();
|
|
2429
2475
|
const tables = {};
|
|
2430
2476
|
if (def.tables) {
|
|
@@ -2456,16 +2502,22 @@ var init_engine = __esm({
|
|
|
2456
2502
|
filesWritten,
|
|
2457
2503
|
counters,
|
|
2458
2504
|
throttle,
|
|
2459
|
-
signal
|
|
2505
|
+
signal,
|
|
2506
|
+
opts.changedTables
|
|
2460
2507
|
);
|
|
2461
2508
|
if (entityContextManifest === null) {
|
|
2462
2509
|
return this._abortedResult(filesWritten, counters, start);
|
|
2463
2510
|
}
|
|
2464
2511
|
if (this._schema.getEntityContexts().size > 0) {
|
|
2512
|
+
let entityContexts = entityContextManifest;
|
|
2513
|
+
if (opts.changedTables) {
|
|
2514
|
+
const prev = readManifest(outputDir);
|
|
2515
|
+
entityContexts = { ...prev?.entityContexts ?? {}, ...entityContextManifest };
|
|
2516
|
+
}
|
|
2465
2517
|
writeManifest(outputDir, {
|
|
2466
2518
|
version: 2,
|
|
2467
2519
|
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2468
|
-
entityContexts
|
|
2520
|
+
entityContexts
|
|
2469
2521
|
});
|
|
2470
2522
|
}
|
|
2471
2523
|
const result = {
|
|
@@ -2531,12 +2583,14 @@ var init_engine = __esm({
|
|
|
2531
2583
|
* partial tree). Progress is reported through `throttle`; abort is observed
|
|
2532
2584
|
* via `signal`.
|
|
2533
2585
|
*/
|
|
2534
|
-
async _renderEntityContexts(outputDir, filesWritten, counters, throttle, signal) {
|
|
2586
|
+
async _renderEntityContexts(outputDir, filesWritten, counters, throttle, signal, changedTables) {
|
|
2535
2587
|
const protectedTables = /* @__PURE__ */ new Set();
|
|
2536
2588
|
for (const [t8, d6] of this._schema.getEntityContexts()) {
|
|
2537
2589
|
if (d6.protected) protectedTables.add(t8);
|
|
2538
2590
|
}
|
|
2539
|
-
const entityTables = [...this._schema.getEntityContexts()]
|
|
2591
|
+
const entityTables = [...this._schema.getEntityContexts()].filter(
|
|
2592
|
+
([table, def]) => !changedTables || this._entityAffected(table, def, changedTables)
|
|
2593
|
+
);
|
|
2540
2594
|
const tableCount = entityTables.length;
|
|
2541
2595
|
if (signal?.aborted) return null;
|
|
2542
2596
|
const renderedEntries = await mapWithConcurrency(
|
|
@@ -5236,6 +5290,16 @@ var init_lattice = __esm({
|
|
|
5236
5290
|
_autoRenderPending = false;
|
|
5237
5291
|
_autoRenderInFlight = false;
|
|
5238
5292
|
_autoRenderDebounceMs = 250;
|
|
5293
|
+
/**
|
|
5294
|
+
* Incremental auto-render scope, accumulated between debounced renders. A write
|
|
5295
|
+
* or a remote (cloud) change records the AFFECTED table here, so the next
|
|
5296
|
+
* auto-render re-renders only that entity (+ its cross-table dependents) instead
|
|
5297
|
+
* of the whole tree. `_pendingRenderAll` forces a full render (the initial
|
|
5298
|
+
* render, or a change with no known table). Captured + reset when a render
|
|
5299
|
+
* starts, so changes during a render re-accumulate and re-trigger.
|
|
5300
|
+
*/
|
|
5301
|
+
_pendingRenderTables = /* @__PURE__ */ new Set();
|
|
5302
|
+
_pendingRenderAll = true;
|
|
5239
5303
|
/** Cache of actual table columns (from PRAGMA), populated after init(). */
|
|
5240
5304
|
_columnCache = /* @__PURE__ */ new Map();
|
|
5241
5305
|
/** Derived encryption key (from options.encryptionKey via scrypt). */
|
|
@@ -6331,7 +6395,7 @@ var init_lattice = __esm({
|
|
|
6331
6395
|
`${verb} INTO "${junctionTable}" (${colNames}) VALUES (${placeholders})`,
|
|
6332
6396
|
Object.values(filtered)
|
|
6333
6397
|
);
|
|
6334
|
-
this._scheduleAutoRender();
|
|
6398
|
+
this._scheduleAutoRender(junctionTable);
|
|
6335
6399
|
}
|
|
6336
6400
|
/**
|
|
6337
6401
|
* Delete rows from a junction table matching all given conditions.
|
|
@@ -6347,7 +6411,7 @@ var init_lattice = __esm({
|
|
|
6347
6411
|
`DELETE FROM "${junctionTable}" WHERE ${where}`,
|
|
6348
6412
|
entries.map(([, v2]) => v2)
|
|
6349
6413
|
);
|
|
6350
|
-
this._scheduleAutoRender();
|
|
6414
|
+
this._scheduleAutoRender(junctionTable);
|
|
6351
6415
|
}
|
|
6352
6416
|
// -------------------------------------------------------------------------
|
|
6353
6417
|
// Seeding DSL (v0.13+)
|
|
@@ -6603,6 +6667,11 @@ var init_lattice = __esm({
|
|
|
6603
6667
|
async renderInBackground(outputDir, opts = {}) {
|
|
6604
6668
|
const notInit = this._notInitError();
|
|
6605
6669
|
if (notInit) return notInit;
|
|
6670
|
+
if (!opts.changedTables) {
|
|
6671
|
+
this._pendingRenderAll = false;
|
|
6672
|
+
this._pendingRenderTables = /* @__PURE__ */ new Set();
|
|
6673
|
+
this._autoRenderPending = false;
|
|
6674
|
+
}
|
|
6606
6675
|
return this._renderGuarded(outputDir, opts);
|
|
6607
6676
|
}
|
|
6608
6677
|
/**
|
|
@@ -6632,9 +6701,12 @@ var init_lattice = __esm({
|
|
|
6632
6701
|
* tree when a REMOTE change arrives — notably an owner re-sharing or un-sharing
|
|
6633
6702
|
* a row, after which the member's per-viewer projection must be recompiled. A
|
|
6634
6703
|
* no-op when auto-render isn't enabled.
|
|
6704
|
+
*
|
|
6705
|
+
* Pass the CHANGED table so only that entity (+ its cross-table dependents) is
|
|
6706
|
+
* re-rendered instead of the whole tree; omit it to force a full render.
|
|
6635
6707
|
*/
|
|
6636
|
-
requestRender() {
|
|
6637
|
-
this._scheduleAutoRender();
|
|
6708
|
+
requestRender(table) {
|
|
6709
|
+
this._scheduleAutoRender(table);
|
|
6638
6710
|
}
|
|
6639
6711
|
/**
|
|
6640
6712
|
* True while a render is actively writing the context tree + manifest (auto-
|
|
@@ -7035,7 +7107,7 @@ var init_lattice = __esm({
|
|
|
7035
7107
|
for (const h6 of this._errorHandlers) h6(err instanceof Error ? err : new Error(String(err)));
|
|
7036
7108
|
}
|
|
7037
7109
|
}
|
|
7038
|
-
this._scheduleAutoRender();
|
|
7110
|
+
this._scheduleAutoRender(table);
|
|
7039
7111
|
}
|
|
7040
7112
|
/**
|
|
7041
7113
|
* Turn on automatic rendering into `outputDir`. After this, every insert /
|
|
@@ -7059,10 +7131,18 @@ var init_lattice = __esm({
|
|
|
7059
7131
|
this._autoRenderPending = false;
|
|
7060
7132
|
return this;
|
|
7061
7133
|
}
|
|
7062
|
-
_scheduleAutoRender() {
|
|
7134
|
+
_scheduleAutoRender(table) {
|
|
7063
7135
|
if (!this._autoRenderDir) return;
|
|
7136
|
+
if (table === void 0) this._pendingRenderAll = true;
|
|
7137
|
+
else this._pendingRenderTables.add(table);
|
|
7064
7138
|
this._autoRenderPending = true;
|
|
7065
|
-
|
|
7139
|
+
this._armAutoRenderTimer();
|
|
7140
|
+
}
|
|
7141
|
+
/** Arm the debounce timer if not already armed. Does NOT change the render
|
|
7142
|
+
* scope — used both by `_scheduleAutoRender` and the post-render re-arm so a
|
|
7143
|
+
* re-arm never escalates a pending incremental render to a full one. */
|
|
7144
|
+
_armAutoRenderTimer() {
|
|
7145
|
+
if (!this._autoRenderDir || this._autoRenderTimer) return;
|
|
7066
7146
|
this._autoRenderTimer = setTimeout(() => {
|
|
7067
7147
|
this._autoRenderTimer = void 0;
|
|
7068
7148
|
void this._runAutoRender();
|
|
@@ -7102,10 +7182,15 @@ var init_lattice = __esm({
|
|
|
7102
7182
|
}
|
|
7103
7183
|
if (!this._autoRenderPending) return;
|
|
7104
7184
|
this._autoRenderPending = false;
|
|
7185
|
+
const renderAll = this._pendingRenderAll;
|
|
7186
|
+
const changed = this._pendingRenderTables;
|
|
7187
|
+
this._pendingRenderAll = false;
|
|
7188
|
+
this._pendingRenderTables = /* @__PURE__ */ new Set();
|
|
7105
7189
|
this._autoRenderInFlight = true;
|
|
7106
7190
|
try {
|
|
7107
7191
|
const prevManifest = readManifest(dir);
|
|
7108
|
-
const
|
|
7192
|
+
const renderOpts = renderAll || changed.size === 0 ? {} : { changedTables: changed };
|
|
7193
|
+
const result = await this._render.render(dir, renderOpts);
|
|
7109
7194
|
for (const h6 of this._renderHandlers) h6(result);
|
|
7110
7195
|
const newManifest = readManifest(dir);
|
|
7111
7196
|
await this._render.cleanup(dir, prevManifest, {}, newManifest);
|
|
@@ -7117,7 +7202,7 @@ var init_lattice = __esm({
|
|
|
7117
7202
|
}
|
|
7118
7203
|
}
|
|
7119
7204
|
_rearmAutoRenderIfPending() {
|
|
7120
|
-
if (this._autoRenderPending) this.
|
|
7205
|
+
if (this._autoRenderPending) this._armAutoRenderTimer();
|
|
7121
7206
|
}
|
|
7122
7207
|
/**
|
|
7123
7208
|
* Update or remove the embedding for a row.
|
|
@@ -47282,6 +47367,44 @@ CREATE TRIGGER "${trg}" AFTER INSERT OR UPDATE OR DELETE ON ${q3}
|
|
|
47282
47367
|
FOR EACH ROW EXECUTE FUNCTION "${trg}"();
|
|
47283
47368
|
`;
|
|
47284
47369
|
}
|
|
47370
|
+
async function ownPolyfillsByGroup(db) {
|
|
47371
|
+
if (!isPg(db)) return;
|
|
47372
|
+
for (const sig of ["json_extract(text, text)", "strftime(text, text)"]) {
|
|
47373
|
+
try {
|
|
47374
|
+
const reg = await getAsyncOrSync(db.adapter, `SELECT to_regprocedure($1) AS reg`, [sig]);
|
|
47375
|
+
if (reg?.reg == null) continue;
|
|
47376
|
+
await runAsyncOrSync(db.adapter, `ALTER FUNCTION ${sig} OWNER TO "${MEMBER_GROUP}"`);
|
|
47377
|
+
} catch {
|
|
47378
|
+
}
|
|
47379
|
+
}
|
|
47380
|
+
}
|
|
47381
|
+
async function enableGuiAuditRls(db) {
|
|
47382
|
+
if (!isPg(db)) return;
|
|
47383
|
+
const reg = await getAsyncOrSync(db.adapter, `SELECT to_regclass($1) AS reg`, [
|
|
47384
|
+
"_lattice_gui_audit"
|
|
47385
|
+
]);
|
|
47386
|
+
if (reg?.reg == null) return;
|
|
47387
|
+
await runCloudBootstrapSql(
|
|
47388
|
+
db,
|
|
47389
|
+
`
|
|
47390
|
+
ALTER TABLE "_lattice_gui_audit" ENABLE ROW LEVEL SECURITY;
|
|
47391
|
+
ALTER TABLE "_lattice_gui_audit" FORCE ROW LEVEL SECURITY;
|
|
47392
|
+
DROP POLICY IF EXISTS "lattice_gui_audit_owner" ON "_lattice_gui_audit";
|
|
47393
|
+
DROP POLICY IF EXISTS "lattice_gui_audit_sel" ON "_lattice_gui_audit";
|
|
47394
|
+
CREATE POLICY "lattice_gui_audit_sel" ON "_lattice_gui_audit" FOR SELECT
|
|
47395
|
+
USING ("row_id" IS NULL OR lattice_row_visible("table_name", "row_id"));
|
|
47396
|
+
DROP POLICY IF EXISTS "lattice_gui_audit_ins" ON "_lattice_gui_audit";
|
|
47397
|
+
CREATE POLICY "lattice_gui_audit_ins" ON "_lattice_gui_audit" FOR INSERT
|
|
47398
|
+
WITH CHECK ("row_id" IS NULL OR lattice_row_visible("table_name", "row_id"));
|
|
47399
|
+
DROP POLICY IF EXISTS "lattice_gui_audit_upd" ON "_lattice_gui_audit";
|
|
47400
|
+
CREATE POLICY "lattice_gui_audit_upd" ON "_lattice_gui_audit" FOR UPDATE
|
|
47401
|
+
USING ("row_id" IS NULL OR lattice_row_visible("table_name", "row_id"));
|
|
47402
|
+
DROP POLICY IF EXISTS "lattice_gui_audit_del" ON "_lattice_gui_audit";
|
|
47403
|
+
CREATE POLICY "lattice_gui_audit_del" ON "_lattice_gui_audit" FOR DELETE
|
|
47404
|
+
USING ("row_id" IS NULL OR lattice_row_visible("table_name", "row_id"));
|
|
47405
|
+
`
|
|
47406
|
+
);
|
|
47407
|
+
}
|
|
47285
47408
|
async function installCloudRls(db) {
|
|
47286
47409
|
if (!isPg(db)) return;
|
|
47287
47410
|
const schema = await cloudSchema(db);
|
|
@@ -47315,6 +47438,24 @@ CREATE POLICY "lattice_changelog_ins" ON "__lattice_changelog" FOR INSERT WITH C
|
|
|
47315
47438
|
`
|
|
47316
47439
|
);
|
|
47317
47440
|
}
|
|
47441
|
+
async function enableChatPrivacyRls(db) {
|
|
47442
|
+
if (!isPg(db)) return;
|
|
47443
|
+
for (const t8 of ["chat_threads", "chat_messages"]) {
|
|
47444
|
+
const reg = await getAsyncOrSync(db.adapter, `SELECT to_regclass($1) AS reg`, [t8]);
|
|
47445
|
+
if (reg?.reg == null) continue;
|
|
47446
|
+
const q3 = `"${t8}"`;
|
|
47447
|
+
await runCloudBootstrapSql(
|
|
47448
|
+
db,
|
|
47449
|
+
`
|
|
47450
|
+
ALTER TABLE ${q3} ENABLE ROW LEVEL SECURITY;
|
|
47451
|
+
ALTER TABLE ${q3} FORCE ROW LEVEL SECURITY;
|
|
47452
|
+
DROP POLICY IF EXISTS "lattice_chat_owner" ON ${q3};
|
|
47453
|
+
CREATE POLICY "lattice_chat_owner" ON ${q3} AS RESTRICTIVE FOR SELECT
|
|
47454
|
+
USING ("owner_user_id" IS NOT NULL AND "owner_user_id" = session_user);
|
|
47455
|
+
`
|
|
47456
|
+
);
|
|
47457
|
+
}
|
|
47458
|
+
}
|
|
47318
47459
|
async function enableRlsForTable(db, table, pkCols) {
|
|
47319
47460
|
if (!isPg(db)) return;
|
|
47320
47461
|
const schema = await cloudSchema(db);
|
|
@@ -47429,6 +47570,18 @@ CREATE TABLE IF NOT EXISTS "__lattice_member_invites" (
|
|
|
47429
47570
|
-- bootstrap is now run directly + idempotently, not version-gated).
|
|
47430
47571
|
ALTER TABLE "__lattice_member_invites" ADD COLUMN IF NOT EXISTS "email" text;
|
|
47431
47572
|
|
|
47573
|
+
-- Owner-published entity/render LAYOUT (the entities + entityContexts config
|
|
47574
|
+
-- blocks), so a joined member \u2014 whose generated config has entities: {} \u2014 can
|
|
47575
|
+
-- hydrate the full render layout and produce a complete context tree. This holds
|
|
47576
|
+
-- schema CONFIG, not row data, so it is safe to share with members (granted
|
|
47577
|
+
-- SELECT). A shared singleton, like __lattice_user_identity: no per-row RLS.
|
|
47578
|
+
CREATE TABLE IF NOT EXISTS "__lattice_shared_schema" (
|
|
47579
|
+
"id" TEXT PRIMARY KEY DEFAULT 'singleton',
|
|
47580
|
+
"entities_json" TEXT,
|
|
47581
|
+
"contexts_json" TEXT,
|
|
47582
|
+
"updated_at" TEXT
|
|
47583
|
+
);
|
|
47584
|
+
|
|
47432
47585
|
-- #3.1 \u2014 one-time-use + revocation enforcement. After a member authenticates to
|
|
47433
47586
|
-- the cloud with their minted credential, the join path calls this to CLAIM the
|
|
47434
47587
|
-- invite. The single atomic UPDATE stamps redeemed_at and returns true ONLY when
|
|
@@ -50357,7 +50510,7 @@ function findDocsDir() {
|
|
|
50357
50510
|
}
|
|
50358
50511
|
for (let i6 = 0; i6 < 8; i6++) {
|
|
50359
50512
|
const candidate = (0, import_node_path31.join)(dir, "docs");
|
|
50360
|
-
if ((0,
|
|
50513
|
+
if ((0, import_node_fs29.existsSync)((0, import_node_path31.join)(candidate, "cloud.md"))) {
|
|
50361
50514
|
_docsDir = candidate;
|
|
50362
50515
|
return _docsDir;
|
|
50363
50516
|
}
|
|
@@ -50398,13 +50551,13 @@ function allSections() {
|
|
|
50398
50551
|
const out = [];
|
|
50399
50552
|
let files = [];
|
|
50400
50553
|
try {
|
|
50401
|
-
files = (0,
|
|
50554
|
+
files = (0, import_node_fs29.readdirSync)(dir).filter((f6) => f6.endsWith(".md"));
|
|
50402
50555
|
} catch {
|
|
50403
50556
|
files = [];
|
|
50404
50557
|
}
|
|
50405
50558
|
for (const f6 of files) {
|
|
50406
50559
|
try {
|
|
50407
|
-
out.push(...sectionsOf(f6, (0,
|
|
50560
|
+
out.push(...sectionsOf(f6, (0, import_node_fs29.readFileSync)((0, import_node_path31.join)(dir, f6), "utf8")));
|
|
50408
50561
|
} catch {
|
|
50409
50562
|
}
|
|
50410
50563
|
}
|
|
@@ -50446,11 +50599,11 @@ function searchLatticeDocs(query, limit = 4) {
|
|
|
50446
50599
|
}))
|
|
50447
50600
|
};
|
|
50448
50601
|
}
|
|
50449
|
-
var
|
|
50602
|
+
var import_node_fs29, import_node_path31, import_node_url2, import_meta5, _docsDir, MAX_SECTION_CHARS, _cache;
|
|
50450
50603
|
var init_lattice_docs = __esm({
|
|
50451
50604
|
"src/gui/ai/lattice-docs.ts"() {
|
|
50452
50605
|
"use strict";
|
|
50453
|
-
|
|
50606
|
+
import_node_fs29 = require("fs");
|
|
50454
50607
|
import_node_path31 = require("path");
|
|
50455
50608
|
import_node_url2 = require("url");
|
|
50456
50609
|
import_meta5 = {};
|
|
@@ -54518,7 +54671,7 @@ var MEMBER_READABLE_BOOKKEEPING = [
|
|
|
54518
54671
|
{
|
|
54519
54672
|
name: "_lattice_gui_audit",
|
|
54520
54673
|
privs: "SELECT, INSERT",
|
|
54521
|
-
why: "
|
|
54674
|
+
why: "GUI undo/redo + version history; RLS (enableGuiAuditRls) scopes reads to entries whose underlying row the member can see"
|
|
54522
54675
|
},
|
|
54523
54676
|
{
|
|
54524
54677
|
name: "__lattice_user_identity",
|
|
@@ -54529,6 +54682,11 @@ var MEMBER_READABLE_BOOKKEEPING = [
|
|
|
54529
54682
|
name: "__lattice_changelog",
|
|
54530
54683
|
privs: "SELECT, INSERT",
|
|
54531
54684
|
why: "per-viewer-RLS-filtered change history for observe()/history (the policy filters reads, so the base grant is safe)"
|
|
54685
|
+
},
|
|
54686
|
+
{
|
|
54687
|
+
name: "__lattice_shared_schema",
|
|
54688
|
+
privs: "SELECT",
|
|
54689
|
+
why: "owner-published entity/render layout (entities + entityContexts) a joined member hydrates its config from so render produces the full context tree"
|
|
54532
54690
|
}
|
|
54533
54691
|
];
|
|
54534
54692
|
var MEMBER_EXECUTE_FUNCTIONS = [
|
|
@@ -54680,9 +54838,12 @@ async function secureCloud(db) {
|
|
|
54680
54838
|
if (db.getDialect() !== "postgres") return;
|
|
54681
54839
|
await registerPostgresPolyfills((sql) => runAsyncOrSync(db.adapter, sql));
|
|
54682
54840
|
await installCloudRls(db);
|
|
54841
|
+
await ownPolyfillsByGroup(db);
|
|
54683
54842
|
await installCloudSettings(db);
|
|
54684
54843
|
await db.ensureObservationSubstrate();
|
|
54685
54844
|
await enableChangelogRls(db);
|
|
54845
|
+
await enableChatPrivacyRls(db);
|
|
54846
|
+
await enableGuiAuditRls(db);
|
|
54686
54847
|
await convergeLegacyColumnAudience(db);
|
|
54687
54848
|
const registered = db.getRegisteredTableNames();
|
|
54688
54849
|
for (const table of registered) {
|
|
@@ -57728,12 +57889,17 @@ var appJs = `
|
|
|
57728
57889
|
fetchJson('/api/gui-meta/columns').catch(function () { return {}; }),
|
|
57729
57890
|
fetchJson('/api/system-tables').catch(function () { return { tables: [] }; }),
|
|
57730
57891
|
fetchJson('/api/workspaces').catch(function () { return null; }),
|
|
57892
|
+
fetchJson('/api/dbconfig').catch(function () { return {}; }),
|
|
57731
57893
|
]).then(function (results) {
|
|
57732
57894
|
state.entities = results[0];
|
|
57733
57895
|
state.iconOverrides = results[1] || {};
|
|
57734
57896
|
state.columnMeta = results[2] || {};
|
|
57735
57897
|
state.systemTables = (results[3] && results[3].tables) || [];
|
|
57736
57898
|
renderWsSwitcher(results[4]);
|
|
57899
|
+
// Re-point the header logo at the NEW workspace's mark \u2014 the switch path
|
|
57900
|
+
// must refresh branding the way boot does (the etag cache-busts the
|
|
57901
|
+
// <img>), else the previous workspace's logo stays until a hard refresh.
|
|
57902
|
+
applyWorkspaceLogo((results[5] || {}).logoEtag);
|
|
57737
57903
|
renderSidebar();
|
|
57738
57904
|
// renderWsSwitcher set cloudMode from the new workspace's kind; re-render
|
|
57739
57905
|
// the composer so the Private-mode toggle reflects local vs cloud (it is
|
|
@@ -64631,6 +64797,87 @@ init_postgres();
|
|
|
64631
64797
|
init_cloud_connect();
|
|
64632
64798
|
init_rls();
|
|
64633
64799
|
|
|
64800
|
+
// src/cloud/shared-schema.ts
|
|
64801
|
+
init_lattice();
|
|
64802
|
+
init_adapter();
|
|
64803
|
+
|
|
64804
|
+
// src/gui/config-io.ts
|
|
64805
|
+
var import_node_fs28 = require("fs");
|
|
64806
|
+
var import_yaml4 = require("yaml");
|
|
64807
|
+
async function execSql(db, sql) {
|
|
64808
|
+
const adapter = db._adapter;
|
|
64809
|
+
if (!adapter.runAsync) throw new Error("Adapter does not support runAsync");
|
|
64810
|
+
await adapter.runAsync(sql);
|
|
64811
|
+
}
|
|
64812
|
+
function loadConfigDoc(configPath) {
|
|
64813
|
+
return (0, import_yaml4.parseDocument)((0, import_node_fs28.readFileSync)(configPath, "utf8"));
|
|
64814
|
+
}
|
|
64815
|
+
function saveConfigDoc(configPath, doc) {
|
|
64816
|
+
(0, import_node_fs28.writeFileSync)(configPath, doc.toString(), "utf8");
|
|
64817
|
+
}
|
|
64818
|
+
|
|
64819
|
+
// src/cloud/shared-schema.ts
|
|
64820
|
+
async function publishSharedSchema(db, configPath) {
|
|
64821
|
+
if (db.getDialect() !== "postgres") return;
|
|
64822
|
+
const cfg = loadConfigDoc(configPath).toJSON();
|
|
64823
|
+
const entities = cfg.entities ?? {};
|
|
64824
|
+
if (Object.keys(entities).length === 0) return;
|
|
64825
|
+
await runAsyncOrSync(
|
|
64826
|
+
db.adapter,
|
|
64827
|
+
`INSERT INTO "__lattice_shared_schema" ("id","entities_json","contexts_json","updated_at")
|
|
64828
|
+
VALUES ('singleton', $1, $2, $3)
|
|
64829
|
+
ON CONFLICT ("id") DO UPDATE SET
|
|
64830
|
+
"entities_json" = EXCLUDED."entities_json",
|
|
64831
|
+
"contexts_json" = EXCLUDED."contexts_json",
|
|
64832
|
+
"updated_at" = EXCLUDED."updated_at"`,
|
|
64833
|
+
[
|
|
64834
|
+
JSON.stringify(entities),
|
|
64835
|
+
JSON.stringify(cfg.entityContexts ?? null),
|
|
64836
|
+
(/* @__PURE__ */ new Date()).toISOString()
|
|
64837
|
+
]
|
|
64838
|
+
);
|
|
64839
|
+
}
|
|
64840
|
+
async function hydrateMemberConfigFromCloud(configPath, dbUrl, encryptionKey) {
|
|
64841
|
+
if (!isPostgresUrl(dbUrl)) return false;
|
|
64842
|
+
const existing = loadConfigDoc(configPath).toJSON();
|
|
64843
|
+
if (Object.keys(existing.entities ?? {}).length > 0) return false;
|
|
64844
|
+
try {
|
|
64845
|
+
const peek = new Lattice({ config: configPath }, { encryptionKey });
|
|
64846
|
+
try {
|
|
64847
|
+
await peek.init({ introspectOnly: true });
|
|
64848
|
+
const reg = await getAsyncOrSync(
|
|
64849
|
+
peek.adapter,
|
|
64850
|
+
"SELECT to_regclass('__lattice_shared_schema') AS reg"
|
|
64851
|
+
);
|
|
64852
|
+
if (reg?.reg == null) return false;
|
|
64853
|
+
const row = await getAsyncOrSync(
|
|
64854
|
+
peek.adapter,
|
|
64855
|
+
'SELECT "entities_json","contexts_json" FROM "__lattice_shared_schema" WHERE "id" = $1',
|
|
64856
|
+
["singleton"]
|
|
64857
|
+
);
|
|
64858
|
+
if (row?.entities_json == null) return false;
|
|
64859
|
+
const entities = JSON.parse(row.entities_json);
|
|
64860
|
+
if (Object.keys(entities).length === 0) return false;
|
|
64861
|
+
const doc = loadConfigDoc(configPath);
|
|
64862
|
+
doc.setIn(["entities"], entities);
|
|
64863
|
+
if (row.contexts_json != null) {
|
|
64864
|
+
const ctx = JSON.parse(row.contexts_json);
|
|
64865
|
+
if (ctx) doc.setIn(["entityContexts"], ctx);
|
|
64866
|
+
}
|
|
64867
|
+
saveConfigDoc(configPath, doc);
|
|
64868
|
+
return true;
|
|
64869
|
+
} finally {
|
|
64870
|
+
peek.close();
|
|
64871
|
+
}
|
|
64872
|
+
} catch (e6) {
|
|
64873
|
+
console.warn(
|
|
64874
|
+
"[hydrateMemberConfigFromCloud] could not hydrate member schema:",
|
|
64875
|
+
e6.message
|
|
64876
|
+
);
|
|
64877
|
+
return false;
|
|
64878
|
+
}
|
|
64879
|
+
}
|
|
64880
|
+
|
|
64634
64881
|
// src/gui/meta-gen.ts
|
|
64635
64882
|
init_assistant_routes();
|
|
64636
64883
|
init_chat();
|
|
@@ -64719,21 +64966,6 @@ var FeedBus = class {
|
|
|
64719
64966
|
init_fts();
|
|
64720
64967
|
init_mutations();
|
|
64721
64968
|
|
|
64722
|
-
// src/gui/config-io.ts
|
|
64723
|
-
var import_node_fs29 = require("fs");
|
|
64724
|
-
var import_yaml4 = require("yaml");
|
|
64725
|
-
async function execSql(db, sql) {
|
|
64726
|
-
const adapter = db._adapter;
|
|
64727
|
-
if (!adapter.runAsync) throw new Error("Adapter does not support runAsync");
|
|
64728
|
-
await adapter.runAsync(sql);
|
|
64729
|
-
}
|
|
64730
|
-
function loadConfigDoc(configPath) {
|
|
64731
|
-
return (0, import_yaml4.parseDocument)((0, import_node_fs29.readFileSync)(configPath, "utf8"));
|
|
64732
|
-
}
|
|
64733
|
-
function saveConfigDoc(configPath, doc) {
|
|
64734
|
-
(0, import_node_fs29.writeFileSync)(configPath, doc.toString(), "utf8");
|
|
64735
|
-
}
|
|
64736
|
-
|
|
64737
64969
|
// src/gui/schema-ops.ts
|
|
64738
64970
|
init_parser();
|
|
64739
64971
|
init_canonical_context();
|
|
@@ -65922,6 +66154,7 @@ async function dispatchDbConfigRoute(req, res, ctx) {
|
|
|
65922
66154
|
const result = await migrateLatticeData(ctx.db, target);
|
|
65923
66155
|
await target.rebuildFtsIndexes();
|
|
65924
66156
|
await secureCloud(target);
|
|
66157
|
+
await publishSharedSchema(target, ctx.configPath);
|
|
65925
66158
|
target.close();
|
|
65926
66159
|
const sourceDbPath = parseConfigFile(ctx.configPath).dbPath;
|
|
65927
66160
|
const backupPath = archiveLocalSqlite(sourceDbPath);
|
|
@@ -66384,6 +66617,7 @@ function activeCloudCoords(configPath) {
|
|
|
66384
66617
|
init_assistant_routes();
|
|
66385
66618
|
|
|
66386
66619
|
// src/gui/chat-routes.ts
|
|
66620
|
+
init_adapter();
|
|
66387
66621
|
init_assistant_routes();
|
|
66388
66622
|
init_chat();
|
|
66389
66623
|
init_user_config();
|
|
@@ -66409,6 +66643,15 @@ function sendJson3(res, body, status = 200) {
|
|
|
66409
66643
|
function asStr(v2, fallback = "") {
|
|
66410
66644
|
return typeof v2 === "string" ? v2 : fallback;
|
|
66411
66645
|
}
|
|
66646
|
+
function isCloudChat(db) {
|
|
66647
|
+
return db.getDialect() === "postgres";
|
|
66648
|
+
}
|
|
66649
|
+
async function resolveChatOwnerId(db) {
|
|
66650
|
+
if (!isCloudChat(db)) return null;
|
|
66651
|
+
const row = await getAsyncOrSync(db.adapter, "SELECT session_user AS u");
|
|
66652
|
+
const u2 = row?.u;
|
|
66653
|
+
return typeof u2 === "string" && u2.length > 0 ? u2 : null;
|
|
66654
|
+
}
|
|
66412
66655
|
function readJson3(req) {
|
|
66413
66656
|
return new Promise((resolve12, reject) => {
|
|
66414
66657
|
let raw = "";
|
|
@@ -66601,12 +66844,20 @@ async function persistMessage(db, threadId, role, text, ownerUserId, turns, star
|
|
|
66601
66844
|
});
|
|
66602
66845
|
}
|
|
66603
66846
|
async function dispatchChatRoute(req, res, ctx) {
|
|
66847
|
+
const ownerUserId = await resolveChatOwnerId(ctx.db);
|
|
66848
|
+
const cloud = isCloudChat(ctx.db);
|
|
66849
|
+
const ownedByMe = (r6) => !cloud || r6.owner_user_id != null && r6.owner_user_id === ownerUserId;
|
|
66604
66850
|
if (ctx.method === "GET" && ctx.pathname === "/api/chat/threads") {
|
|
66851
|
+
if (cloud && ownerUserId == null) {
|
|
66852
|
+
sendJson3(res, { threads: [] });
|
|
66853
|
+
return true;
|
|
66854
|
+
}
|
|
66605
66855
|
const filters = [
|
|
66606
66856
|
{ col: "deleted_at", op: "isNull" }
|
|
66607
66857
|
];
|
|
66858
|
+
if (ownerUserId != null) filters.push({ col: "owner_user_id", op: "eq", val: ownerUserId });
|
|
66608
66859
|
const rows = await ctx.db.query("chat_threads", { filters, limit: 100 });
|
|
66609
|
-
const threads = rows.filter((r6) => !r6.deleted_at).map((r6) => ({
|
|
66860
|
+
const threads = rows.filter((r6) => !r6.deleted_at && ownedByMe(r6)).map((r6) => ({
|
|
66610
66861
|
id: asStr(r6.id),
|
|
66611
66862
|
title: asStr(r6.title, "Chat"),
|
|
66612
66863
|
created_at: asStr(r6.created_at)
|
|
@@ -66617,12 +66868,19 @@ async function dispatchChatRoute(req, res, ctx) {
|
|
|
66617
66868
|
const msgMatch = /^\/api\/chat\/threads\/([^/]+)\/messages$/.exec(ctx.pathname);
|
|
66618
66869
|
if (ctx.method === "GET" && msgMatch) {
|
|
66619
66870
|
const threadId2 = decodeURIComponent(msgMatch[1] ?? "");
|
|
66620
|
-
|
|
66871
|
+
if (cloud && ownerUserId == null) {
|
|
66872
|
+
sendJson3(res, { messages: [] });
|
|
66873
|
+
return true;
|
|
66874
|
+
}
|
|
66875
|
+
const msgFilters = [
|
|
66876
|
+
{ col: "thread_id", op: "eq", val: threadId2 }
|
|
66877
|
+
];
|
|
66878
|
+
if (ownerUserId != null) msgFilters.push({ col: "owner_user_id", op: "eq", val: ownerUserId });
|
|
66621
66879
|
const rows = await ctx.db.query("chat_messages", {
|
|
66622
66880
|
filters: msgFilters,
|
|
66623
66881
|
limit: 1e3
|
|
66624
66882
|
});
|
|
66625
|
-
const messages = rows.filter((r6) => r6.thread_id === threadId2 && !r6.deleted_at).map((r6) => {
|
|
66883
|
+
const messages = rows.filter((r6) => r6.thread_id === threadId2 && !r6.deleted_at && ownedByMe(r6)).map((r6) => {
|
|
66626
66884
|
let text = "";
|
|
66627
66885
|
let turns2;
|
|
66628
66886
|
let startedAt;
|
|
@@ -66675,12 +66933,21 @@ async function dispatchChatRoute(req, res, ctx) {
|
|
|
66675
66933
|
return true;
|
|
66676
66934
|
}
|
|
66677
66935
|
const requestedThread = typeof body.threadId === "string" ? body.threadId : null;
|
|
66936
|
+
if (cloud && ownerUserId == null) {
|
|
66937
|
+
sendJson3(res, { error: "Could not resolve your cloud identity; chat is disabled." }, 500);
|
|
66938
|
+
return true;
|
|
66939
|
+
}
|
|
66678
66940
|
const activeContext = parseActiveContext(body.activeContext, ctx.validTables);
|
|
66679
|
-
const history = await rehydrateHistory(
|
|
66941
|
+
const history = await rehydrateHistory(
|
|
66942
|
+
ctx.db,
|
|
66943
|
+
requestedThread,
|
|
66944
|
+
mapHistory(body.history),
|
|
66945
|
+
ownerUserId
|
|
66946
|
+
);
|
|
66680
66947
|
let threadId = "";
|
|
66681
66948
|
try {
|
|
66682
|
-
threadId = await ensureThread(ctx.db, requestedThread, message,
|
|
66683
|
-
await persistMessage(ctx.db, threadId, "user", message,
|
|
66949
|
+
threadId = await ensureThread(ctx.db, requestedThread, message, ownerUserId);
|
|
66950
|
+
await persistMessage(ctx.db, threadId, "user", message, ownerUserId);
|
|
66684
66951
|
} catch (e6) {
|
|
66685
66952
|
console.warn("[chat] persist user message failed:", e6.message);
|
|
66686
66953
|
}
|
|
@@ -66752,7 +67019,7 @@ async function dispatchChatRoute(req, res, ctx) {
|
|
|
66752
67019
|
threadId,
|
|
66753
67020
|
"assistant",
|
|
66754
67021
|
assistantText,
|
|
66755
|
-
|
|
67022
|
+
ownerUserId,
|
|
66756
67023
|
cleanTurns,
|
|
66757
67024
|
turnStartedAt,
|
|
66758
67025
|
assistantMsgId
|
|
@@ -67596,10 +67863,13 @@ function resolveOutputDirForConfig(configPath) {
|
|
|
67596
67863
|
async function openConfig(configPath, outputDir, autoRender = false, realtimeWatchdogMs) {
|
|
67597
67864
|
healRawDbUrl(configPath);
|
|
67598
67865
|
const parsed = parseConfigFile(configPath);
|
|
67866
|
+
const encryptionKey = getOrCreateMasterKey();
|
|
67599
67867
|
if (!/^postgres(ql)?:\/\//i.test(parsed.dbPath) && !parsed.dbPath.startsWith("file:") && parsed.dbPath !== ":memory:") {
|
|
67600
67868
|
(0, import_node_fs35.mkdirSync)((0, import_node_path38.dirname)(parsed.dbPath), { recursive: true });
|
|
67601
67869
|
}
|
|
67602
|
-
|
|
67870
|
+
if (/^postgres(ql)?:\/\//i.test(parsed.dbPath)) {
|
|
67871
|
+
await hydrateMemberConfigFromCloud(configPath, parsed.dbPath, encryptionKey);
|
|
67872
|
+
}
|
|
67603
67873
|
const db = new Lattice({ config: configPath }, { encryptionKey });
|
|
67604
67874
|
registerNativeEntities(db);
|
|
67605
67875
|
db.define("_lattice_gui_meta", {
|
|
@@ -67751,19 +68021,32 @@ async function openConfig(configPath, outputDir, autoRender = false, realtimeWat
|
|
|
67751
68021
|
if (await cloudRlsInstalled(db) && await canManageRoles(db)) {
|
|
67752
68022
|
await registerPostgresPolyfills((sql) => runAsyncOrSync(db.adapter, sql));
|
|
67753
68023
|
await installCloudRls(db);
|
|
68024
|
+
await ownPolyfillsByGroup(db);
|
|
67754
68025
|
await installCloudSettings(db);
|
|
67755
68026
|
await db.ensureObservationSubstrate();
|
|
67756
68027
|
await enableChangelogRls(db);
|
|
68028
|
+
await enableChatPrivacyRls(db);
|
|
68029
|
+
await enableGuiAuditRls(db);
|
|
67757
68030
|
const access = await reconcileCloudMemberAccess(db);
|
|
67758
68031
|
convergeWarnings = access.skipped;
|
|
67759
68032
|
for (const s2 of convergeWarnings) {
|
|
67760
68033
|
console.warn(`[openConfig] cloud converge could not manage "${s2.table}": ${s2.reason}`);
|
|
67761
68034
|
}
|
|
68035
|
+
await publishSharedSchema(db, configPath);
|
|
67762
68036
|
}
|
|
67763
68037
|
} catch (e6) {
|
|
67764
68038
|
console.error("[openConfig] cloud bootstrap converge failed:", e6.message);
|
|
67765
68039
|
}
|
|
67766
68040
|
}
|
|
68041
|
+
if (memberOpen) {
|
|
68042
|
+
const userTables = db.getRegisteredTableNames().filter((t8) => !t8.startsWith("_") && !discoveredJunctions.has(t8));
|
|
68043
|
+
if (userTables.length === 0) {
|
|
68044
|
+
convergeWarnings.push({
|
|
68045
|
+
table: "(schema)",
|
|
68046
|
+
reason: "No entity layout is configured for this cloud workspace yet \u2014 ask the cloud owner to open the workspace once so it publishes the schema, then reopen. Until then, render produces no context files."
|
|
68047
|
+
});
|
|
68048
|
+
}
|
|
68049
|
+
}
|
|
67767
68050
|
const validTables = new Set(parsed.tables.map((t8) => t8.name));
|
|
67768
68051
|
for (const name of db.getRegisteredTableNames()) {
|
|
67769
68052
|
if (name.startsWith("__lattice_") || name.startsWith("_lattice_")) continue;
|
|
@@ -67915,11 +68198,22 @@ function startBackgroundRender(active) {
|
|
|
67915
68198
|
active.eagerRenderWired = true;
|
|
67916
68199
|
let lastFire = 0;
|
|
67917
68200
|
let trailing;
|
|
68201
|
+
const pendingTables = /* @__PURE__ */ new Set();
|
|
68202
|
+
let pendingFull = false;
|
|
67918
68203
|
const fire = () => {
|
|
67919
68204
|
lastFire = Date.now();
|
|
67920
|
-
|
|
68205
|
+
if (pendingFull || pendingTables.size === 0) {
|
|
68206
|
+
pendingFull = false;
|
|
68207
|
+
pendingTables.clear();
|
|
68208
|
+
active.db.requestRender();
|
|
68209
|
+
return;
|
|
68210
|
+
}
|
|
68211
|
+
for (const t8 of pendingTables) active.db.requestRender(t8);
|
|
68212
|
+
pendingTables.clear();
|
|
67921
68213
|
};
|
|
67922
|
-
active.realtime.subscribePayload(() => {
|
|
68214
|
+
active.realtime.subscribePayload((payload) => {
|
|
68215
|
+
if (payload.table_name) pendingTables.add(payload.table_name);
|
|
68216
|
+
else pendingFull = true;
|
|
67923
68217
|
const since = Date.now() - lastFire;
|
|
67924
68218
|
if (since >= EAGER_RERENDER_MIN_INTERVAL_MS) {
|
|
67925
68219
|
fire();
|