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.js
CHANGED
|
@@ -908,43 +908,57 @@ var init_postgres = __esm({
|
|
|
908
908
|
},
|
|
909
909
|
{
|
|
910
910
|
warn: "could not register json_extract polyfill:",
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
911
|
+
// Create ONLY if absent. `CREATE OR REPLACE` on an existing function requires
|
|
912
|
+
// ownership, but on a cloud the function is owned by whichever single role
|
|
913
|
+
// created it first — so every OTHER member's per-connect replace raised "must
|
|
914
|
+
// be owner of function" and (sharing the render transaction) aborted it,
|
|
915
|
+
// yielding an empty render. The IF-absent guard makes a present function a
|
|
916
|
+
// clean no-op for everyone, regardless of who owns it.
|
|
917
|
+
sql: `DO $do$ BEGIN
|
|
918
|
+
IF to_regprocedure('json_extract(text, text)') IS NULL THEN
|
|
919
|
+
CREATE FUNCTION json_extract(doc text, path text)
|
|
920
|
+
RETURNS text
|
|
921
|
+
LANGUAGE sql
|
|
922
|
+
IMMUTABLE
|
|
923
|
+
AS $fn$
|
|
924
|
+
SELECT doc::jsonb #>> string_to_array(regexp_replace(path, '^\\$\\.?', ''), '.')
|
|
925
|
+
$fn$;
|
|
926
|
+
END IF;
|
|
927
|
+
END $do$;`
|
|
918
928
|
},
|
|
919
929
|
{
|
|
920
930
|
warn: "could not register strftime polyfill:",
|
|
921
|
-
sql: `
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
931
|
+
sql: `DO $do$ BEGIN
|
|
932
|
+
IF to_regprocedure('strftime(text, text)') IS NULL THEN
|
|
933
|
+
CREATE FUNCTION strftime(format text, modifier text)
|
|
934
|
+
RETURNS text
|
|
935
|
+
LANGUAGE plpgsql
|
|
936
|
+
IMMUTABLE
|
|
937
|
+
AS $fn$
|
|
938
|
+
DECLARE ts timestamptz;
|
|
939
|
+
BEGIN
|
|
940
|
+
IF modifier = 'now' THEN
|
|
941
|
+
ts := now();
|
|
942
|
+
ELSE
|
|
943
|
+
ts := modifier::timestamptz;
|
|
944
|
+
END IF;
|
|
945
|
+
RETURN to_char(
|
|
946
|
+
ts AT TIME ZONE 'UTC',
|
|
947
|
+
replace(replace(replace(replace(replace(replace(replace(replace(
|
|
948
|
+
format,
|
|
949
|
+
'%Y', 'YYYY'),
|
|
950
|
+
'%m', 'MM'),
|
|
951
|
+
'%d', 'DD'),
|
|
952
|
+
'%H', 'HH24'),
|
|
953
|
+
'%M', 'MI'),
|
|
954
|
+
'%S', 'SS'),
|
|
955
|
+
'%f', 'MS'),
|
|
956
|
+
'T', '"T"')
|
|
957
|
+
);
|
|
958
|
+
END;
|
|
959
|
+
$fn$;
|
|
960
|
+
END IF;
|
|
961
|
+
END $do$;`
|
|
948
962
|
}
|
|
949
963
|
];
|
|
950
964
|
}
|
|
@@ -2352,6 +2366,34 @@ var init_engine = __esm({
|
|
|
2352
2366
|
setRenderFold(fn) {
|
|
2353
2367
|
this._foldRows = fn;
|
|
2354
2368
|
}
|
|
2369
|
+
/**
|
|
2370
|
+
* Incremental scope: is this entity-context table affected by a change to one
|
|
2371
|
+
* of `changed`? Affected when the table itself changed (its own rows / `self`
|
|
2372
|
+
* source / index) OR any of its files SOURCES from a changed table (a cross-
|
|
2373
|
+
* table dependent — e.g. an AGENT.md that lists the agent's tasks must re-render
|
|
2374
|
+
* when `tasks` changes). A `custom` source runs an arbitrary query, so we can't
|
|
2375
|
+
* prove independence — treat it as always-affected (conservative, never stale).
|
|
2376
|
+
*/
|
|
2377
|
+
_entityAffected(table, def, changed) {
|
|
2378
|
+
if (changed.has(table)) return true;
|
|
2379
|
+
for (const spec of Object.values(def.files)) {
|
|
2380
|
+
if (this._sourceTouches(spec.source, changed)) return true;
|
|
2381
|
+
}
|
|
2382
|
+
return false;
|
|
2383
|
+
}
|
|
2384
|
+
_sourceTouches(source, changed) {
|
|
2385
|
+
if (source == null || typeof source !== "object") return false;
|
|
2386
|
+
const s2 = source;
|
|
2387
|
+
if (s2.type === "custom") return true;
|
|
2388
|
+
if (typeof s2.table === "string" && changed.has(s2.table)) return true;
|
|
2389
|
+
if (typeof s2.junctionTable === "string" && changed.has(s2.junctionTable)) return true;
|
|
2390
|
+
if (s2.sources != null && typeof s2.sources === "object") {
|
|
2391
|
+
for (const sub of Object.values(s2.sources)) {
|
|
2392
|
+
if (this._sourceTouches(sub, changed)) return true;
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
return false;
|
|
2396
|
+
}
|
|
2355
2397
|
async render(outputDir, opts = {}) {
|
|
2356
2398
|
const start = Date.now();
|
|
2357
2399
|
const filesWritten = [];
|
|
@@ -2361,6 +2403,7 @@ var init_engine = __esm({
|
|
|
2361
2403
|
for (const [name, def] of this._schema.getTables()) {
|
|
2362
2404
|
if (signal?.aborted) return this._abortedResult(filesWritten, counters, start);
|
|
2363
2405
|
if (this._skipEmpty && def.render === NOOP_RENDER) continue;
|
|
2406
|
+
if (opts.changedTables && !opts.changedTables.has(name)) continue;
|
|
2364
2407
|
let rows = await this._schema.queryTable(this._adapter, name, this._readRel);
|
|
2365
2408
|
if (def.relevanceFilter) {
|
|
2366
2409
|
const ctx = this._getTaskContext();
|
|
@@ -2413,6 +2456,9 @@ var init_engine = __esm({
|
|
|
2413
2456
|
}
|
|
2414
2457
|
for (const [name, def] of this._schema.getMultis()) {
|
|
2415
2458
|
if (signal?.aborted) return this._abortedResult(filesWritten, counters, start);
|
|
2459
|
+
if (opts.changedTables && def.tables && !def.tables.some((t8) => opts.changedTables?.has(t8))) {
|
|
2460
|
+
continue;
|
|
2461
|
+
}
|
|
2416
2462
|
const keys = await def.keys();
|
|
2417
2463
|
const tables = {};
|
|
2418
2464
|
if (def.tables) {
|
|
@@ -2444,16 +2490,22 @@ var init_engine = __esm({
|
|
|
2444
2490
|
filesWritten,
|
|
2445
2491
|
counters,
|
|
2446
2492
|
throttle,
|
|
2447
|
-
signal
|
|
2493
|
+
signal,
|
|
2494
|
+
opts.changedTables
|
|
2448
2495
|
);
|
|
2449
2496
|
if (entityContextManifest === null) {
|
|
2450
2497
|
return this._abortedResult(filesWritten, counters, start);
|
|
2451
2498
|
}
|
|
2452
2499
|
if (this._schema.getEntityContexts().size > 0) {
|
|
2500
|
+
let entityContexts = entityContextManifest;
|
|
2501
|
+
if (opts.changedTables) {
|
|
2502
|
+
const prev = readManifest(outputDir);
|
|
2503
|
+
entityContexts = { ...prev?.entityContexts ?? {}, ...entityContextManifest };
|
|
2504
|
+
}
|
|
2453
2505
|
writeManifest(outputDir, {
|
|
2454
2506
|
version: 2,
|
|
2455
2507
|
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2456
|
-
entityContexts
|
|
2508
|
+
entityContexts
|
|
2457
2509
|
});
|
|
2458
2510
|
}
|
|
2459
2511
|
const result = {
|
|
@@ -2519,12 +2571,14 @@ var init_engine = __esm({
|
|
|
2519
2571
|
* partial tree). Progress is reported through `throttle`; abort is observed
|
|
2520
2572
|
* via `signal`.
|
|
2521
2573
|
*/
|
|
2522
|
-
async _renderEntityContexts(outputDir, filesWritten, counters, throttle, signal) {
|
|
2574
|
+
async _renderEntityContexts(outputDir, filesWritten, counters, throttle, signal, changedTables) {
|
|
2523
2575
|
const protectedTables = /* @__PURE__ */ new Set();
|
|
2524
2576
|
for (const [t8, d6] of this._schema.getEntityContexts()) {
|
|
2525
2577
|
if (d6.protected) protectedTables.add(t8);
|
|
2526
2578
|
}
|
|
2527
|
-
const entityTables = [...this._schema.getEntityContexts()]
|
|
2579
|
+
const entityTables = [...this._schema.getEntityContexts()].filter(
|
|
2580
|
+
([table, def]) => !changedTables || this._entityAffected(table, def, changedTables)
|
|
2581
|
+
);
|
|
2528
2582
|
const tableCount = entityTables.length;
|
|
2529
2583
|
if (signal?.aborted) return null;
|
|
2530
2584
|
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.
|
|
@@ -47275,6 +47360,44 @@ CREATE TRIGGER "${trg}" AFTER INSERT OR UPDATE OR DELETE ON ${q3}
|
|
|
47275
47360
|
FOR EACH ROW EXECUTE FUNCTION "${trg}"();
|
|
47276
47361
|
`;
|
|
47277
47362
|
}
|
|
47363
|
+
async function ownPolyfillsByGroup(db) {
|
|
47364
|
+
if (!isPg(db)) return;
|
|
47365
|
+
for (const sig of ["json_extract(text, text)", "strftime(text, text)"]) {
|
|
47366
|
+
try {
|
|
47367
|
+
const reg = await getAsyncOrSync(db.adapter, `SELECT to_regprocedure($1) AS reg`, [sig]);
|
|
47368
|
+
if (reg?.reg == null) continue;
|
|
47369
|
+
await runAsyncOrSync(db.adapter, `ALTER FUNCTION ${sig} OWNER TO "${MEMBER_GROUP}"`);
|
|
47370
|
+
} catch {
|
|
47371
|
+
}
|
|
47372
|
+
}
|
|
47373
|
+
}
|
|
47374
|
+
async function enableGuiAuditRls(db) {
|
|
47375
|
+
if (!isPg(db)) return;
|
|
47376
|
+
const reg = await getAsyncOrSync(db.adapter, `SELECT to_regclass($1) AS reg`, [
|
|
47377
|
+
"_lattice_gui_audit"
|
|
47378
|
+
]);
|
|
47379
|
+
if (reg?.reg == null) return;
|
|
47380
|
+
await runCloudBootstrapSql(
|
|
47381
|
+
db,
|
|
47382
|
+
`
|
|
47383
|
+
ALTER TABLE "_lattice_gui_audit" ENABLE ROW LEVEL SECURITY;
|
|
47384
|
+
ALTER TABLE "_lattice_gui_audit" FORCE ROW LEVEL SECURITY;
|
|
47385
|
+
DROP POLICY IF EXISTS "lattice_gui_audit_owner" ON "_lattice_gui_audit";
|
|
47386
|
+
DROP POLICY IF EXISTS "lattice_gui_audit_sel" ON "_lattice_gui_audit";
|
|
47387
|
+
CREATE POLICY "lattice_gui_audit_sel" ON "_lattice_gui_audit" FOR SELECT
|
|
47388
|
+
USING ("row_id" IS NULL OR lattice_row_visible("table_name", "row_id"));
|
|
47389
|
+
DROP POLICY IF EXISTS "lattice_gui_audit_ins" ON "_lattice_gui_audit";
|
|
47390
|
+
CREATE POLICY "lattice_gui_audit_ins" ON "_lattice_gui_audit" FOR INSERT
|
|
47391
|
+
WITH CHECK ("row_id" IS NULL OR lattice_row_visible("table_name", "row_id"));
|
|
47392
|
+
DROP POLICY IF EXISTS "lattice_gui_audit_upd" ON "_lattice_gui_audit";
|
|
47393
|
+
CREATE POLICY "lattice_gui_audit_upd" ON "_lattice_gui_audit" FOR UPDATE
|
|
47394
|
+
USING ("row_id" IS NULL OR lattice_row_visible("table_name", "row_id"));
|
|
47395
|
+
DROP POLICY IF EXISTS "lattice_gui_audit_del" ON "_lattice_gui_audit";
|
|
47396
|
+
CREATE POLICY "lattice_gui_audit_del" ON "_lattice_gui_audit" FOR DELETE
|
|
47397
|
+
USING ("row_id" IS NULL OR lattice_row_visible("table_name", "row_id"));
|
|
47398
|
+
`
|
|
47399
|
+
);
|
|
47400
|
+
}
|
|
47278
47401
|
async function installCloudRls(db) {
|
|
47279
47402
|
if (!isPg(db)) return;
|
|
47280
47403
|
const schema = await cloudSchema(db);
|
|
@@ -47308,6 +47431,24 @@ CREATE POLICY "lattice_changelog_ins" ON "__lattice_changelog" FOR INSERT WITH C
|
|
|
47308
47431
|
`
|
|
47309
47432
|
);
|
|
47310
47433
|
}
|
|
47434
|
+
async function enableChatPrivacyRls(db) {
|
|
47435
|
+
if (!isPg(db)) return;
|
|
47436
|
+
for (const t8 of ["chat_threads", "chat_messages"]) {
|
|
47437
|
+
const reg = await getAsyncOrSync(db.adapter, `SELECT to_regclass($1) AS reg`, [t8]);
|
|
47438
|
+
if (reg?.reg == null) continue;
|
|
47439
|
+
const q3 = `"${t8}"`;
|
|
47440
|
+
await runCloudBootstrapSql(
|
|
47441
|
+
db,
|
|
47442
|
+
`
|
|
47443
|
+
ALTER TABLE ${q3} ENABLE ROW LEVEL SECURITY;
|
|
47444
|
+
ALTER TABLE ${q3} FORCE ROW LEVEL SECURITY;
|
|
47445
|
+
DROP POLICY IF EXISTS "lattice_chat_owner" ON ${q3};
|
|
47446
|
+
CREATE POLICY "lattice_chat_owner" ON ${q3} AS RESTRICTIVE FOR SELECT
|
|
47447
|
+
USING ("owner_user_id" IS NOT NULL AND "owner_user_id" = session_user);
|
|
47448
|
+
`
|
|
47449
|
+
);
|
|
47450
|
+
}
|
|
47451
|
+
}
|
|
47311
47452
|
async function enableRlsForTable(db, table, pkCols) {
|
|
47312
47453
|
if (!isPg(db)) return;
|
|
47313
47454
|
const schema = await cloudSchema(db);
|
|
@@ -47422,6 +47563,18 @@ CREATE TABLE IF NOT EXISTS "__lattice_member_invites" (
|
|
|
47422
47563
|
-- bootstrap is now run directly + idempotently, not version-gated).
|
|
47423
47564
|
ALTER TABLE "__lattice_member_invites" ADD COLUMN IF NOT EXISTS "email" text;
|
|
47424
47565
|
|
|
47566
|
+
-- Owner-published entity/render LAYOUT (the entities + entityContexts config
|
|
47567
|
+
-- blocks), so a joined member \u2014 whose generated config has entities: {} \u2014 can
|
|
47568
|
+
-- hydrate the full render layout and produce a complete context tree. This holds
|
|
47569
|
+
-- schema CONFIG, not row data, so it is safe to share with members (granted
|
|
47570
|
+
-- SELECT). A shared singleton, like __lattice_user_identity: no per-row RLS.
|
|
47571
|
+
CREATE TABLE IF NOT EXISTS "__lattice_shared_schema" (
|
|
47572
|
+
"id" TEXT PRIMARY KEY DEFAULT 'singleton',
|
|
47573
|
+
"entities_json" TEXT,
|
|
47574
|
+
"contexts_json" TEXT,
|
|
47575
|
+
"updated_at" TEXT
|
|
47576
|
+
);
|
|
47577
|
+
|
|
47425
47578
|
-- #3.1 \u2014 one-time-use + revocation enforcement. After a member authenticates to
|
|
47426
47579
|
-- the cloud with their minted credential, the join path calls this to CLAIM the
|
|
47427
47580
|
-- invite. The single atomic UPDATE stamps redeemed_at and returns true ONLY when
|
|
@@ -50338,7 +50491,7 @@ var init_registry = __esm({
|
|
|
50338
50491
|
});
|
|
50339
50492
|
|
|
50340
50493
|
// src/gui/ai/lattice-docs.ts
|
|
50341
|
-
import { readFileSync as
|
|
50494
|
+
import { readFileSync as readFileSync17, readdirSync as readdirSync5, existsSync as existsSync20 } from "fs";
|
|
50342
50495
|
import { dirname as dirname8, join as join25 } from "path";
|
|
50343
50496
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
50344
50497
|
function findDocsDir() {
|
|
@@ -50398,7 +50551,7 @@ function allSections() {
|
|
|
50398
50551
|
}
|
|
50399
50552
|
for (const f6 of files) {
|
|
50400
50553
|
try {
|
|
50401
|
-
out.push(...sectionsOf(f6,
|
|
50554
|
+
out.push(...sectionsOf(f6, readFileSync17(join25(dir, f6), "utf8")));
|
|
50402
50555
|
} catch {
|
|
50403
50556
|
}
|
|
50404
50557
|
}
|
|
@@ -54336,7 +54489,7 @@ var MEMBER_READABLE_BOOKKEEPING = [
|
|
|
54336
54489
|
{
|
|
54337
54490
|
name: "_lattice_gui_audit",
|
|
54338
54491
|
privs: "SELECT, INSERT",
|
|
54339
|
-
why: "
|
|
54492
|
+
why: "GUI undo/redo + version history; RLS (enableGuiAuditRls) scopes reads to entries whose underlying row the member can see"
|
|
54340
54493
|
},
|
|
54341
54494
|
{
|
|
54342
54495
|
name: "__lattice_user_identity",
|
|
@@ -54347,6 +54500,11 @@ var MEMBER_READABLE_BOOKKEEPING = [
|
|
|
54347
54500
|
name: "__lattice_changelog",
|
|
54348
54501
|
privs: "SELECT, INSERT",
|
|
54349
54502
|
why: "per-viewer-RLS-filtered change history for observe()/history (the policy filters reads, so the base grant is safe)"
|
|
54503
|
+
},
|
|
54504
|
+
{
|
|
54505
|
+
name: "__lattice_shared_schema",
|
|
54506
|
+
privs: "SELECT",
|
|
54507
|
+
why: "owner-published entity/render layout (entities + entityContexts) a joined member hydrates its config from so render produces the full context tree"
|
|
54350
54508
|
}
|
|
54351
54509
|
];
|
|
54352
54510
|
var MEMBER_EXECUTE_FUNCTIONS = [
|
|
@@ -54498,9 +54656,12 @@ async function secureCloud(db) {
|
|
|
54498
54656
|
if (db.getDialect() !== "postgres") return;
|
|
54499
54657
|
await registerPostgresPolyfills((sql) => runAsyncOrSync(db.adapter, sql));
|
|
54500
54658
|
await installCloudRls(db);
|
|
54659
|
+
await ownPolyfillsByGroup(db);
|
|
54501
54660
|
await installCloudSettings(db);
|
|
54502
54661
|
await db.ensureObservationSubstrate();
|
|
54503
54662
|
await enableChangelogRls(db);
|
|
54663
|
+
await enableChatPrivacyRls(db);
|
|
54664
|
+
await enableGuiAuditRls(db);
|
|
54504
54665
|
await convergeLegacyColumnAudience(db);
|
|
54505
54666
|
const registered = db.getRegisteredTableNames();
|
|
54506
54667
|
for (const table of registered) {
|
|
@@ -57553,12 +57714,17 @@ var appJs = `
|
|
|
57553
57714
|
fetchJson('/api/gui-meta/columns').catch(function () { return {}; }),
|
|
57554
57715
|
fetchJson('/api/system-tables').catch(function () { return { tables: [] }; }),
|
|
57555
57716
|
fetchJson('/api/workspaces').catch(function () { return null; }),
|
|
57717
|
+
fetchJson('/api/dbconfig').catch(function () { return {}; }),
|
|
57556
57718
|
]).then(function (results) {
|
|
57557
57719
|
state.entities = results[0];
|
|
57558
57720
|
state.iconOverrides = results[1] || {};
|
|
57559
57721
|
state.columnMeta = results[2] || {};
|
|
57560
57722
|
state.systemTables = (results[3] && results[3].tables) || [];
|
|
57561
57723
|
renderWsSwitcher(results[4]);
|
|
57724
|
+
// Re-point the header logo at the NEW workspace's mark \u2014 the switch path
|
|
57725
|
+
// must refresh branding the way boot does (the etag cache-busts the
|
|
57726
|
+
// <img>), else the previous workspace's logo stays until a hard refresh.
|
|
57727
|
+
applyWorkspaceLogo((results[5] || {}).logoEtag);
|
|
57562
57728
|
renderSidebar();
|
|
57563
57729
|
// renderWsSwitcher set cloudMode from the new workspace's kind; re-render
|
|
57564
57730
|
// the composer so the Private-mode toggle reflects local vs cloud (it is
|
|
@@ -64455,6 +64621,87 @@ init_postgres();
|
|
|
64455
64621
|
init_cloud_connect();
|
|
64456
64622
|
init_rls();
|
|
64457
64623
|
|
|
64624
|
+
// src/cloud/shared-schema.ts
|
|
64625
|
+
init_lattice();
|
|
64626
|
+
init_adapter();
|
|
64627
|
+
|
|
64628
|
+
// src/gui/config-io.ts
|
|
64629
|
+
import { readFileSync as readFileSync16, writeFileSync as writeFileSync5 } from "fs";
|
|
64630
|
+
import { parseDocument as parseDocument2 } from "yaml";
|
|
64631
|
+
async function execSql(db, sql) {
|
|
64632
|
+
const adapter = db._adapter;
|
|
64633
|
+
if (!adapter.runAsync) throw new Error("Adapter does not support runAsync");
|
|
64634
|
+
await adapter.runAsync(sql);
|
|
64635
|
+
}
|
|
64636
|
+
function loadConfigDoc(configPath) {
|
|
64637
|
+
return parseDocument2(readFileSync16(configPath, "utf8"));
|
|
64638
|
+
}
|
|
64639
|
+
function saveConfigDoc(configPath, doc) {
|
|
64640
|
+
writeFileSync5(configPath, doc.toString(), "utf8");
|
|
64641
|
+
}
|
|
64642
|
+
|
|
64643
|
+
// src/cloud/shared-schema.ts
|
|
64644
|
+
async function publishSharedSchema(db, configPath) {
|
|
64645
|
+
if (db.getDialect() !== "postgres") return;
|
|
64646
|
+
const cfg = loadConfigDoc(configPath).toJSON();
|
|
64647
|
+
const entities = cfg.entities ?? {};
|
|
64648
|
+
if (Object.keys(entities).length === 0) return;
|
|
64649
|
+
await runAsyncOrSync(
|
|
64650
|
+
db.adapter,
|
|
64651
|
+
`INSERT INTO "__lattice_shared_schema" ("id","entities_json","contexts_json","updated_at")
|
|
64652
|
+
VALUES ('singleton', $1, $2, $3)
|
|
64653
|
+
ON CONFLICT ("id") DO UPDATE SET
|
|
64654
|
+
"entities_json" = EXCLUDED."entities_json",
|
|
64655
|
+
"contexts_json" = EXCLUDED."contexts_json",
|
|
64656
|
+
"updated_at" = EXCLUDED."updated_at"`,
|
|
64657
|
+
[
|
|
64658
|
+
JSON.stringify(entities),
|
|
64659
|
+
JSON.stringify(cfg.entityContexts ?? null),
|
|
64660
|
+
(/* @__PURE__ */ new Date()).toISOString()
|
|
64661
|
+
]
|
|
64662
|
+
);
|
|
64663
|
+
}
|
|
64664
|
+
async function hydrateMemberConfigFromCloud(configPath, dbUrl, encryptionKey) {
|
|
64665
|
+
if (!isPostgresUrl(dbUrl)) return false;
|
|
64666
|
+
const existing = loadConfigDoc(configPath).toJSON();
|
|
64667
|
+
if (Object.keys(existing.entities ?? {}).length > 0) return false;
|
|
64668
|
+
try {
|
|
64669
|
+
const peek = new Lattice({ config: configPath }, { encryptionKey });
|
|
64670
|
+
try {
|
|
64671
|
+
await peek.init({ introspectOnly: true });
|
|
64672
|
+
const reg = await getAsyncOrSync(
|
|
64673
|
+
peek.adapter,
|
|
64674
|
+
"SELECT to_regclass('__lattice_shared_schema') AS reg"
|
|
64675
|
+
);
|
|
64676
|
+
if (reg?.reg == null) return false;
|
|
64677
|
+
const row = await getAsyncOrSync(
|
|
64678
|
+
peek.adapter,
|
|
64679
|
+
'SELECT "entities_json","contexts_json" FROM "__lattice_shared_schema" WHERE "id" = $1',
|
|
64680
|
+
["singleton"]
|
|
64681
|
+
);
|
|
64682
|
+
if (row?.entities_json == null) return false;
|
|
64683
|
+
const entities = JSON.parse(row.entities_json);
|
|
64684
|
+
if (Object.keys(entities).length === 0) return false;
|
|
64685
|
+
const doc = loadConfigDoc(configPath);
|
|
64686
|
+
doc.setIn(["entities"], entities);
|
|
64687
|
+
if (row.contexts_json != null) {
|
|
64688
|
+
const ctx = JSON.parse(row.contexts_json);
|
|
64689
|
+
if (ctx) doc.setIn(["entityContexts"], ctx);
|
|
64690
|
+
}
|
|
64691
|
+
saveConfigDoc(configPath, doc);
|
|
64692
|
+
return true;
|
|
64693
|
+
} finally {
|
|
64694
|
+
peek.close();
|
|
64695
|
+
}
|
|
64696
|
+
} catch (e6) {
|
|
64697
|
+
console.warn(
|
|
64698
|
+
"[hydrateMemberConfigFromCloud] could not hydrate member schema:",
|
|
64699
|
+
e6.message
|
|
64700
|
+
);
|
|
64701
|
+
return false;
|
|
64702
|
+
}
|
|
64703
|
+
}
|
|
64704
|
+
|
|
64458
64705
|
// src/gui/meta-gen.ts
|
|
64459
64706
|
init_assistant_routes();
|
|
64460
64707
|
init_chat();
|
|
@@ -64543,21 +64790,6 @@ var FeedBus = class {
|
|
|
64543
64790
|
init_fts();
|
|
64544
64791
|
init_mutations();
|
|
64545
64792
|
|
|
64546
|
-
// src/gui/config-io.ts
|
|
64547
|
-
import { readFileSync as readFileSync17, writeFileSync as writeFileSync5 } from "fs";
|
|
64548
|
-
import { parseDocument as parseDocument2 } from "yaml";
|
|
64549
|
-
async function execSql(db, sql) {
|
|
64550
|
-
const adapter = db._adapter;
|
|
64551
|
-
if (!adapter.runAsync) throw new Error("Adapter does not support runAsync");
|
|
64552
|
-
await adapter.runAsync(sql);
|
|
64553
|
-
}
|
|
64554
|
-
function loadConfigDoc(configPath) {
|
|
64555
|
-
return parseDocument2(readFileSync17(configPath, "utf8"));
|
|
64556
|
-
}
|
|
64557
|
-
function saveConfigDoc(configPath, doc) {
|
|
64558
|
-
writeFileSync5(configPath, doc.toString(), "utf8");
|
|
64559
|
-
}
|
|
64560
|
-
|
|
64561
64793
|
// src/gui/schema-ops.ts
|
|
64562
64794
|
init_parser();
|
|
64563
64795
|
init_canonical_context();
|
|
@@ -65746,6 +65978,7 @@ async function dispatchDbConfigRoute(req, res, ctx) {
|
|
|
65746
65978
|
const result = await migrateLatticeData(ctx.db, target);
|
|
65747
65979
|
await target.rebuildFtsIndexes();
|
|
65748
65980
|
await secureCloud(target);
|
|
65981
|
+
await publishSharedSchema(target, ctx.configPath);
|
|
65749
65982
|
target.close();
|
|
65750
65983
|
const sourceDbPath = parseConfigFile(ctx.configPath).dbPath;
|
|
65751
65984
|
const backupPath = archiveLocalSqlite(sourceDbPath);
|
|
@@ -66208,6 +66441,7 @@ function activeCloudCoords(configPath) {
|
|
|
66208
66441
|
init_assistant_routes();
|
|
66209
66442
|
|
|
66210
66443
|
// src/gui/chat-routes.ts
|
|
66444
|
+
init_adapter();
|
|
66211
66445
|
init_assistant_routes();
|
|
66212
66446
|
init_chat();
|
|
66213
66447
|
init_user_config();
|
|
@@ -66233,6 +66467,15 @@ function sendJson3(res, body, status = 200) {
|
|
|
66233
66467
|
function asStr(v2, fallback = "") {
|
|
66234
66468
|
return typeof v2 === "string" ? v2 : fallback;
|
|
66235
66469
|
}
|
|
66470
|
+
function isCloudChat(db) {
|
|
66471
|
+
return db.getDialect() === "postgres";
|
|
66472
|
+
}
|
|
66473
|
+
async function resolveChatOwnerId(db) {
|
|
66474
|
+
if (!isCloudChat(db)) return null;
|
|
66475
|
+
const row = await getAsyncOrSync(db.adapter, "SELECT session_user AS u");
|
|
66476
|
+
const u2 = row?.u;
|
|
66477
|
+
return typeof u2 === "string" && u2.length > 0 ? u2 : null;
|
|
66478
|
+
}
|
|
66236
66479
|
function readJson3(req) {
|
|
66237
66480
|
return new Promise((resolve12, reject) => {
|
|
66238
66481
|
let raw = "";
|
|
@@ -66425,12 +66668,20 @@ async function persistMessage(db, threadId, role, text, ownerUserId, turns, star
|
|
|
66425
66668
|
});
|
|
66426
66669
|
}
|
|
66427
66670
|
async function dispatchChatRoute(req, res, ctx) {
|
|
66671
|
+
const ownerUserId = await resolveChatOwnerId(ctx.db);
|
|
66672
|
+
const cloud = isCloudChat(ctx.db);
|
|
66673
|
+
const ownedByMe = (r6) => !cloud || r6.owner_user_id != null && r6.owner_user_id === ownerUserId;
|
|
66428
66674
|
if (ctx.method === "GET" && ctx.pathname === "/api/chat/threads") {
|
|
66675
|
+
if (cloud && ownerUserId == null) {
|
|
66676
|
+
sendJson3(res, { threads: [] });
|
|
66677
|
+
return true;
|
|
66678
|
+
}
|
|
66429
66679
|
const filters = [
|
|
66430
66680
|
{ col: "deleted_at", op: "isNull" }
|
|
66431
66681
|
];
|
|
66682
|
+
if (ownerUserId != null) filters.push({ col: "owner_user_id", op: "eq", val: ownerUserId });
|
|
66432
66683
|
const rows = await ctx.db.query("chat_threads", { filters, limit: 100 });
|
|
66433
|
-
const threads = rows.filter((r6) => !r6.deleted_at).map((r6) => ({
|
|
66684
|
+
const threads = rows.filter((r6) => !r6.deleted_at && ownedByMe(r6)).map((r6) => ({
|
|
66434
66685
|
id: asStr(r6.id),
|
|
66435
66686
|
title: asStr(r6.title, "Chat"),
|
|
66436
66687
|
created_at: asStr(r6.created_at)
|
|
@@ -66441,12 +66692,19 @@ async function dispatchChatRoute(req, res, ctx) {
|
|
|
66441
66692
|
const msgMatch = /^\/api\/chat\/threads\/([^/]+)\/messages$/.exec(ctx.pathname);
|
|
66442
66693
|
if (ctx.method === "GET" && msgMatch) {
|
|
66443
66694
|
const threadId2 = decodeURIComponent(msgMatch[1] ?? "");
|
|
66444
|
-
|
|
66695
|
+
if (cloud && ownerUserId == null) {
|
|
66696
|
+
sendJson3(res, { messages: [] });
|
|
66697
|
+
return true;
|
|
66698
|
+
}
|
|
66699
|
+
const msgFilters = [
|
|
66700
|
+
{ col: "thread_id", op: "eq", val: threadId2 }
|
|
66701
|
+
];
|
|
66702
|
+
if (ownerUserId != null) msgFilters.push({ col: "owner_user_id", op: "eq", val: ownerUserId });
|
|
66445
66703
|
const rows = await ctx.db.query("chat_messages", {
|
|
66446
66704
|
filters: msgFilters,
|
|
66447
66705
|
limit: 1e3
|
|
66448
66706
|
});
|
|
66449
|
-
const messages = rows.filter((r6) => r6.thread_id === threadId2 && !r6.deleted_at).map((r6) => {
|
|
66707
|
+
const messages = rows.filter((r6) => r6.thread_id === threadId2 && !r6.deleted_at && ownedByMe(r6)).map((r6) => {
|
|
66450
66708
|
let text = "";
|
|
66451
66709
|
let turns2;
|
|
66452
66710
|
let startedAt;
|
|
@@ -66499,12 +66757,21 @@ async function dispatchChatRoute(req, res, ctx) {
|
|
|
66499
66757
|
return true;
|
|
66500
66758
|
}
|
|
66501
66759
|
const requestedThread = typeof body.threadId === "string" ? body.threadId : null;
|
|
66760
|
+
if (cloud && ownerUserId == null) {
|
|
66761
|
+
sendJson3(res, { error: "Could not resolve your cloud identity; chat is disabled." }, 500);
|
|
66762
|
+
return true;
|
|
66763
|
+
}
|
|
66502
66764
|
const activeContext = parseActiveContext(body.activeContext, ctx.validTables);
|
|
66503
|
-
const history = await rehydrateHistory(
|
|
66765
|
+
const history = await rehydrateHistory(
|
|
66766
|
+
ctx.db,
|
|
66767
|
+
requestedThread,
|
|
66768
|
+
mapHistory(body.history),
|
|
66769
|
+
ownerUserId
|
|
66770
|
+
);
|
|
66504
66771
|
let threadId = "";
|
|
66505
66772
|
try {
|
|
66506
|
-
threadId = await ensureThread(ctx.db, requestedThread, message,
|
|
66507
|
-
await persistMessage(ctx.db, threadId, "user", message,
|
|
66773
|
+
threadId = await ensureThread(ctx.db, requestedThread, message, ownerUserId);
|
|
66774
|
+
await persistMessage(ctx.db, threadId, "user", message, ownerUserId);
|
|
66508
66775
|
} catch (e6) {
|
|
66509
66776
|
console.warn("[chat] persist user message failed:", e6.message);
|
|
66510
66777
|
}
|
|
@@ -66576,7 +66843,7 @@ async function dispatchChatRoute(req, res, ctx) {
|
|
|
66576
66843
|
threadId,
|
|
66577
66844
|
"assistant",
|
|
66578
66845
|
assistantText,
|
|
66579
|
-
|
|
66846
|
+
ownerUserId,
|
|
66580
66847
|
cleanTurns,
|
|
66581
66848
|
turnStartedAt,
|
|
66582
66849
|
assistantMsgId
|
|
@@ -67420,10 +67687,13 @@ function resolveOutputDirForConfig(configPath) {
|
|
|
67420
67687
|
async function openConfig(configPath, outputDir, autoRender = false, realtimeWatchdogMs) {
|
|
67421
67688
|
healRawDbUrl(configPath);
|
|
67422
67689
|
const parsed = parseConfigFile(configPath);
|
|
67690
|
+
const encryptionKey = getOrCreateMasterKey();
|
|
67423
67691
|
if (!/^postgres(ql)?:\/\//i.test(parsed.dbPath) && !parsed.dbPath.startsWith("file:") && parsed.dbPath !== ":memory:") {
|
|
67424
67692
|
mkdirSync10(dirname13(parsed.dbPath), { recursive: true });
|
|
67425
67693
|
}
|
|
67426
|
-
|
|
67694
|
+
if (/^postgres(ql)?:\/\//i.test(parsed.dbPath)) {
|
|
67695
|
+
await hydrateMemberConfigFromCloud(configPath, parsed.dbPath, encryptionKey);
|
|
67696
|
+
}
|
|
67427
67697
|
const db = new Lattice({ config: configPath }, { encryptionKey });
|
|
67428
67698
|
registerNativeEntities(db);
|
|
67429
67699
|
db.define("_lattice_gui_meta", {
|
|
@@ -67575,19 +67845,32 @@ async function openConfig(configPath, outputDir, autoRender = false, realtimeWat
|
|
|
67575
67845
|
if (await cloudRlsInstalled(db) && await canManageRoles(db)) {
|
|
67576
67846
|
await registerPostgresPolyfills((sql) => runAsyncOrSync(db.adapter, sql));
|
|
67577
67847
|
await installCloudRls(db);
|
|
67848
|
+
await ownPolyfillsByGroup(db);
|
|
67578
67849
|
await installCloudSettings(db);
|
|
67579
67850
|
await db.ensureObservationSubstrate();
|
|
67580
67851
|
await enableChangelogRls(db);
|
|
67852
|
+
await enableChatPrivacyRls(db);
|
|
67853
|
+
await enableGuiAuditRls(db);
|
|
67581
67854
|
const access = await reconcileCloudMemberAccess(db);
|
|
67582
67855
|
convergeWarnings = access.skipped;
|
|
67583
67856
|
for (const s2 of convergeWarnings) {
|
|
67584
67857
|
console.warn(`[openConfig] cloud converge could not manage "${s2.table}": ${s2.reason}`);
|
|
67585
67858
|
}
|
|
67859
|
+
await publishSharedSchema(db, configPath);
|
|
67586
67860
|
}
|
|
67587
67861
|
} catch (e6) {
|
|
67588
67862
|
console.error("[openConfig] cloud bootstrap converge failed:", e6.message);
|
|
67589
67863
|
}
|
|
67590
67864
|
}
|
|
67865
|
+
if (memberOpen) {
|
|
67866
|
+
const userTables = db.getRegisteredTableNames().filter((t8) => !t8.startsWith("_") && !discoveredJunctions.has(t8));
|
|
67867
|
+
if (userTables.length === 0) {
|
|
67868
|
+
convergeWarnings.push({
|
|
67869
|
+
table: "(schema)",
|
|
67870
|
+
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."
|
|
67871
|
+
});
|
|
67872
|
+
}
|
|
67873
|
+
}
|
|
67591
67874
|
const validTables = new Set(parsed.tables.map((t8) => t8.name));
|
|
67592
67875
|
for (const name of db.getRegisteredTableNames()) {
|
|
67593
67876
|
if (name.startsWith("__lattice_") || name.startsWith("_lattice_")) continue;
|
|
@@ -67739,11 +68022,22 @@ function startBackgroundRender(active) {
|
|
|
67739
68022
|
active.eagerRenderWired = true;
|
|
67740
68023
|
let lastFire = 0;
|
|
67741
68024
|
let trailing;
|
|
68025
|
+
const pendingTables = /* @__PURE__ */ new Set();
|
|
68026
|
+
let pendingFull = false;
|
|
67742
68027
|
const fire = () => {
|
|
67743
68028
|
lastFire = Date.now();
|
|
67744
|
-
|
|
68029
|
+
if (pendingFull || pendingTables.size === 0) {
|
|
68030
|
+
pendingFull = false;
|
|
68031
|
+
pendingTables.clear();
|
|
68032
|
+
active.db.requestRender();
|
|
68033
|
+
return;
|
|
68034
|
+
}
|
|
68035
|
+
for (const t8 of pendingTables) active.db.requestRender(t8);
|
|
68036
|
+
pendingTables.clear();
|
|
67745
68037
|
};
|
|
67746
|
-
active.realtime.subscribePayload(() => {
|
|
68038
|
+
active.realtime.subscribePayload((payload) => {
|
|
68039
|
+
if (payload.table_name) pendingTables.add(payload.table_name);
|
|
68040
|
+
else pendingFull = true;
|
|
67747
68041
|
const since = Date.now() - lastFire;
|
|
67748
68042
|
if (since >= EAGER_RERENDER_MIN_INTERVAL_MS) {
|
|
67749
68043
|
fire();
|