@mirrowel/opencode-openshare 0.2.0-dev.1 → 0.2.0-dev.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/README.md +36 -37
- package/ci/README.md +9 -3
- package/ci/openshare.js +379 -85
- package/dist/binding-queue.d.ts +1 -0
- package/dist/binding-queue.js +22 -0
- package/dist/commands.js +41 -285
- package/dist/core.js +524 -88
- package/dist/dialogs.js +1 -1
- package/dist/settings.js +141 -16
- package/dist/share-flow.d.ts +7 -2
- package/dist/share-flow.js +32 -17
- package/dist/sidebar.js +0 -21
- package/dist/tui.js +256 -386
- package/dist/v2-server.js +55 -0
- package/package.json +1 -1
- package/src/binding-queue.ts +26 -0
- package/src/commands.ts +41 -280
- package/src/core.ts +505 -87
- package/src/dialogs.tsx +11 -2
- package/src/settings.ts +133 -15
- package/src/share-flow.ts +33 -19
- package/src/sidebar.tsx +0 -24
- package/src/v2-server.ts +50 -0
package/ci/openshare.js
CHANGED
|
@@ -345,6 +345,7 @@ function resolveShareMode(cfg, input) {
|
|
|
345
345
|
|
|
346
346
|
// src/core.ts
|
|
347
347
|
import { appendFileSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
348
|
+
import { createHash } from "node:crypto";
|
|
348
349
|
import { join as join2 } from "node:path";
|
|
349
350
|
|
|
350
351
|
// src/contract.ts
|
|
@@ -554,6 +555,15 @@ function cursorFor(id, time) {
|
|
|
554
555
|
return Buffer.from(JSON.stringify({ id, time })).toString("base64url");
|
|
555
556
|
}
|
|
556
557
|
|
|
558
|
+
// src/binding-queue.ts
|
|
559
|
+
var queues = new Map;
|
|
560
|
+
function queueBindingWrite(sid, fn) {
|
|
561
|
+
const prev = queues.get(sid) ?? Promise.resolve();
|
|
562
|
+
const run = prev.then(fn, fn);
|
|
563
|
+
queues.set(sid, run.catch(() => {}));
|
|
564
|
+
return run;
|
|
565
|
+
}
|
|
566
|
+
|
|
557
567
|
// src/core.ts
|
|
558
568
|
var DEFAULT_POLICY = {
|
|
559
569
|
mode: "persistent",
|
|
@@ -749,6 +759,8 @@ function createEngine(deps) {
|
|
|
749
759
|
initTimer: null,
|
|
750
760
|
initWasAttach: false,
|
|
751
761
|
backoff: 1000,
|
|
762
|
+
backoffResetTimer: null,
|
|
763
|
+
spoolSaveTimer: null,
|
|
752
764
|
reconnectTimer: null,
|
|
753
765
|
intentionalClose: false,
|
|
754
766
|
connectQueued: false,
|
|
@@ -806,10 +818,14 @@ function createEngine(deps) {
|
|
|
806
818
|
if (b && typeof b === "object" && b.policy) {
|
|
807
819
|
applyPolicy(b.policy, url);
|
|
808
820
|
}
|
|
821
|
+
if (b && typeof b === "object" && b.frameAuth) {
|
|
822
|
+
frameAuthUrls.add(url);
|
|
823
|
+
}
|
|
809
824
|
}).catch(() => {
|
|
810
825
|
policyAsked.delete(url);
|
|
811
826
|
});
|
|
812
827
|
};
|
|
828
|
+
const frameAuthUrls = new Set;
|
|
813
829
|
const wsUrl = (url) => url.replace(/^http/, "ws");
|
|
814
830
|
let spoolRoot = spoolDirFor(cfg);
|
|
815
831
|
try {
|
|
@@ -845,7 +861,8 @@ function createEngine(deps) {
|
|
|
845
861
|
activated: false,
|
|
846
862
|
wroteBinding: false,
|
|
847
863
|
everConnected: false,
|
|
848
|
-
merged: new Map
|
|
864
|
+
merged: new Map,
|
|
865
|
+
sentHash: new Map
|
|
849
866
|
});
|
|
850
867
|
const refreshConfig = () => {
|
|
851
868
|
const next = loadConfig(deps.pluginDir, deps.options);
|
|
@@ -878,6 +895,7 @@ function createEngine(deps) {
|
|
|
878
895
|
}
|
|
879
896
|
st.activated = true;
|
|
880
897
|
st.opts = opts;
|
|
898
|
+
st.sentHash.clear();
|
|
881
899
|
const gated = mode === "manual" || opts2?.gated === true;
|
|
882
900
|
if (gated) {
|
|
883
901
|
chain = chain.then(() => seedManualStatus(st)).then(() => backfill(st, info)).then(() => queueConnectAll(st)).catch((e) => logError("activation error", e?.message || e));
|
|
@@ -929,24 +947,26 @@ function createEngine(deps) {
|
|
|
929
947
|
async function writeMetadata(st, value) {
|
|
930
948
|
if (!st.client)
|
|
931
949
|
return;
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
+
await queueBindingWrite(st.sid, async () => {
|
|
951
|
+
try {
|
|
952
|
+
const info = unwrap(await st.client.session.get({
|
|
953
|
+
path: { id: st.sid },
|
|
954
|
+
...deps.directory ? { query: { directory: deps.directory } } : {}
|
|
955
|
+
}));
|
|
956
|
+
const metadata = { ...info?.metadata ?? {} };
|
|
957
|
+
if (value === null)
|
|
958
|
+
delete metadata[METADATA_KEY];
|
|
959
|
+
else
|
|
960
|
+
metadata[METADATA_KEY] = value;
|
|
961
|
+
unwrap(await st.client.session.update({
|
|
962
|
+
path: { id: st.sid },
|
|
963
|
+
body: { metadata },
|
|
964
|
+
...deps.directory ? { query: { directory: deps.directory } } : {}
|
|
965
|
+
}));
|
|
966
|
+
} catch (e) {
|
|
967
|
+
log("metadata write failed (spool still covers reconnects):", e?.message || e);
|
|
968
|
+
}
|
|
969
|
+
});
|
|
950
970
|
}
|
|
951
971
|
const parseBinding = (record) => {
|
|
952
972
|
const out = new Map;
|
|
@@ -1050,21 +1070,33 @@ function createEngine(deps) {
|
|
|
1050
1070
|
const spoolSave = (st, r) => {
|
|
1051
1071
|
try {
|
|
1052
1072
|
writeFileSync2(metaPath(st, r.url), JSON.stringify({ shareId: r.shareId, secret: r.secret, ackedRev: r.ackedRev, sid: st.sid, url: r.url }));
|
|
1053
|
-
writeFileSync2(queuePath(st, r.url), r.pending.map((f) => JSON.stringify(f)).join(`
|
|
1073
|
+
writeFileSync2(queuePath(st, r.url), r.pending.map((f) => f.wire ?? JSON.stringify(f)).join(`
|
|
1054
1074
|
`) + (r.pending.length ? `
|
|
1055
1075
|
` : ""));
|
|
1076
|
+
rmSync(legacyMetaPath(st), { force: true });
|
|
1077
|
+
rmSync(legacyQueuePath(st), { force: true });
|
|
1056
1078
|
} catch (e) {
|
|
1057
1079
|
logError("spool save failed", e?.message || e);
|
|
1058
1080
|
}
|
|
1059
1081
|
};
|
|
1060
1082
|
const spoolAppend = (st, r, f) => {
|
|
1061
1083
|
try {
|
|
1062
|
-
appendFileSync(queuePath(st, r.url), JSON.stringify(f) + `
|
|
1084
|
+
appendFileSync(queuePath(st, r.url), (f.wire ?? JSON.stringify(f)) + `
|
|
1063
1085
|
`);
|
|
1064
1086
|
} catch (e) {
|
|
1087
|
+
r.spoolAppendFailed = true;
|
|
1065
1088
|
logError("spool append failed", e?.message || e);
|
|
1066
1089
|
}
|
|
1067
1090
|
};
|
|
1091
|
+
const scheduleSpoolSave = (st, r) => {
|
|
1092
|
+
if (r.spoolSaveTimer)
|
|
1093
|
+
return;
|
|
1094
|
+
r.spoolSaveTimer = setTimeout(() => {
|
|
1095
|
+
r.spoolSaveTimer = null;
|
|
1096
|
+
if (!st.closed)
|
|
1097
|
+
spoolSave(st, r);
|
|
1098
|
+
}, 300);
|
|
1099
|
+
};
|
|
1068
1100
|
const spoolLoad = (st, r) => {
|
|
1069
1101
|
try {
|
|
1070
1102
|
const meta = JSON.parse(readFileSync2(metaPath(st, r.url), "utf8"));
|
|
@@ -1073,23 +1105,33 @@ function createEngine(deps) {
|
|
|
1073
1105
|
r.secret = meta.secret;
|
|
1074
1106
|
r.ackedRev = meta.ackedRev ?? 0;
|
|
1075
1107
|
}
|
|
1108
|
+
} catch {}
|
|
1109
|
+
try {
|
|
1076
1110
|
if (existsSync2(queuePath(st, r.url))) {
|
|
1077
|
-
|
|
1111
|
+
const disk = [];
|
|
1078
1112
|
for (const line of readFileSync2(queuePath(st, r.url), "utf8").split(`
|
|
1079
1113
|
`)) {
|
|
1080
1114
|
if (!line.trim())
|
|
1081
1115
|
continue;
|
|
1082
1116
|
try {
|
|
1083
|
-
|
|
1117
|
+
disk.push(JSON.parse(line));
|
|
1084
1118
|
} catch {
|
|
1085
1119
|
log(`spool load: dropped torn tail line for ${st.sid.slice(0, 18)}`);
|
|
1086
1120
|
break;
|
|
1087
1121
|
}
|
|
1088
1122
|
}
|
|
1123
|
+
for (const f of disk) {
|
|
1124
|
+
if (typeof f.seq !== "number")
|
|
1125
|
+
f.seq = ++nextSeq;
|
|
1126
|
+
else if (f.seq >= nextSeq)
|
|
1127
|
+
nextSeq = f.seq + 1;
|
|
1128
|
+
}
|
|
1129
|
+
if (!r.spoolAppendFailed)
|
|
1130
|
+
r.pending = disk;
|
|
1131
|
+
else
|
|
1132
|
+
r.spoolAppendFailed = false;
|
|
1089
1133
|
}
|
|
1090
|
-
} catch {
|
|
1091
|
-
r.pending = [];
|
|
1092
|
-
}
|
|
1134
|
+
} catch {}
|
|
1093
1135
|
if (!r.shareId && targets[0]?.url === r.url) {
|
|
1094
1136
|
try {
|
|
1095
1137
|
const meta = JSON.parse(readFileSync2(legacyMetaPath(st), "utf8"));
|
|
@@ -1108,10 +1150,17 @@ function createEngine(deps) {
|
|
|
1108
1150
|
};
|
|
1109
1151
|
const spoolClear = (st, r) => {
|
|
1110
1152
|
try {
|
|
1153
|
+
if (r.spoolSaveTimer) {
|
|
1154
|
+
clearTimeout(r.spoolSaveTimer);
|
|
1155
|
+
r.spoolSaveTimer = null;
|
|
1156
|
+
}
|
|
1111
1157
|
rmSync(metaPath(st, r.url), { force: true });
|
|
1112
1158
|
rmSync(queuePath(st, r.url), { force: true });
|
|
1159
|
+
rmSync(legacyMetaPath(st), { force: true });
|
|
1160
|
+
rmSync(legacyQueuePath(st), { force: true });
|
|
1113
1161
|
} catch {}
|
|
1114
1162
|
};
|
|
1163
|
+
let nextSeq = 0;
|
|
1115
1164
|
const wsSend = (r, obj) => {
|
|
1116
1165
|
if (debugEvents)
|
|
1117
1166
|
log(`ws ${r.url.slice(0, 24)} #${r.socketSeq} send ${obj?.op} (ready=${r.wsReady})`);
|
|
@@ -1122,30 +1171,50 @@ function createEngine(deps) {
|
|
|
1122
1171
|
}
|
|
1123
1172
|
return true;
|
|
1124
1173
|
};
|
|
1125
|
-
const
|
|
1174
|
+
const chunkedFrames = (items, maxItems, maxBytes) => {
|
|
1126
1175
|
const chunks = [];
|
|
1127
1176
|
let cur = [];
|
|
1177
|
+
let curJson = [];
|
|
1128
1178
|
let curBytes = 0;
|
|
1129
1179
|
for (const it of items) {
|
|
1130
|
-
const
|
|
1131
|
-
if (cur.length && (cur.length >= maxItems || curBytes +
|
|
1132
|
-
chunks.push(cur);
|
|
1180
|
+
const j = JSON.stringify(it) ?? "null";
|
|
1181
|
+
if (cur.length && (cur.length >= maxItems || curBytes + j.length > maxBytes)) {
|
|
1182
|
+
chunks.push({ items: cur, json: "[" + curJson.join(",") + "]" });
|
|
1133
1183
|
cur = [];
|
|
1184
|
+
curJson = [];
|
|
1134
1185
|
curBytes = 0;
|
|
1135
1186
|
}
|
|
1136
1187
|
cur.push(it);
|
|
1137
|
-
|
|
1188
|
+
curJson.push(j);
|
|
1189
|
+
curBytes += j.length;
|
|
1138
1190
|
}
|
|
1139
1191
|
if (cur.length)
|
|
1140
|
-
chunks.push(cur);
|
|
1192
|
+
chunks.push({ items: cur, json: "[" + curJson.join(",") + "]" });
|
|
1141
1193
|
return chunks;
|
|
1142
1194
|
};
|
|
1195
|
+
const chunkItems = (items, maxItems, maxBytes) => chunkedFrames(items, maxItems, maxBytes).map((c) => c.items);
|
|
1196
|
+
const syncFrameStr = (itemsJson, status, seq) => `{"op":"sync","items":${itemsJson},"status":${JSON.stringify(status)}${seq !== undefined ? `,"seq":${seq}` : ""}}`;
|
|
1197
|
+
const wsSendRaw = (r, s) => {
|
|
1198
|
+
if (debugEvents)
|
|
1199
|
+
log(`ws ${r.url.slice(0, 24)} #${r.socketSeq} send raw (${Math.round(s.length / 1024)}KB, ready=${r.wsReady})`);
|
|
1200
|
+
try {
|
|
1201
|
+
r.ws?.send(s);
|
|
1202
|
+
} catch {
|
|
1203
|
+
return false;
|
|
1204
|
+
}
|
|
1205
|
+
return true;
|
|
1206
|
+
};
|
|
1143
1207
|
const sendSync = (r, f) => {
|
|
1144
1208
|
if (!f.items.length)
|
|
1145
|
-
return wsSend(r, { op: "sync", items: [], status: f.status });
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1209
|
+
return wsSend(r, { op: "sync", items: [], status: f.status, seq: f.seq });
|
|
1210
|
+
if (f.wire)
|
|
1211
|
+
return wsSendRaw(r, f.wire);
|
|
1212
|
+
const itemsJson = JSON.stringify(f.items);
|
|
1213
|
+
if (f.items.length <= 200 && itemsJson.length <= 512 * 1024) {
|
|
1214
|
+
return wsSendRaw(r, syncFrameStr(itemsJson, f.status, f.seq));
|
|
1215
|
+
}
|
|
1216
|
+
for (const c of chunkedFrames(f.items, 200, 512 * 1024))
|
|
1217
|
+
wsSendRaw(r, syncFrameStr(c.json, f.status, f.seq));
|
|
1149
1218
|
return true;
|
|
1150
1219
|
};
|
|
1151
1220
|
const announceUrl = (st, r) => {
|
|
@@ -1287,7 +1356,8 @@ function createEngine(deps) {
|
|
|
1287
1356
|
logError(`sharing to ${r.url} disabled (no credential): ${cred.error}`);
|
|
1288
1357
|
return;
|
|
1289
1358
|
}
|
|
1290
|
-
const
|
|
1359
|
+
const useFrameAuth = !!token && frameAuthUrls.has(r.url);
|
|
1360
|
+
const ws = new WebSocket(`${wsUrl(r.url)}/api/ws${useFrameAuth ? "" : `?token=${encodeURIComponent(token ?? "")}`}`);
|
|
1291
1361
|
r.ws = ws;
|
|
1292
1362
|
await new Promise((resolve, reject) => {
|
|
1293
1363
|
const t = setTimeout(() => reject(new Error("ws open timeout")), 15000);
|
|
@@ -1323,6 +1393,27 @@ function createEngine(deps) {
|
|
|
1323
1393
|
if (!st.closed)
|
|
1324
1394
|
scheduleReconnect(st, r);
|
|
1325
1395
|
});
|
|
1396
|
+
if (useFrameAuth) {
|
|
1397
|
+
wsSend(r, { op: "auth", token });
|
|
1398
|
+
const authed = await new Promise((resolve) => {
|
|
1399
|
+
const t = setTimeout(() => resolve(false), 3000);
|
|
1400
|
+
r.authWaiter = () => {
|
|
1401
|
+
clearTimeout(t);
|
|
1402
|
+
resolve(true);
|
|
1403
|
+
};
|
|
1404
|
+
});
|
|
1405
|
+
r.authWaiter = undefined;
|
|
1406
|
+
if (!authed) {
|
|
1407
|
+
logError(`ws ${r.url.slice(0, 24)}: frame-auth ack timeout - reconnecting with query token`);
|
|
1408
|
+
frameAuthUrls.delete(r.url);
|
|
1409
|
+
try {
|
|
1410
|
+
ws.close(4000, "auth timeout");
|
|
1411
|
+
} catch {}
|
|
1412
|
+
if (!st.closed)
|
|
1413
|
+
scheduleReconnect(st, r);
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1326
1417
|
if (r.initWasAttach) {
|
|
1327
1418
|
wsSend(r, { op: "init", attach: { id: r.shareId, secret: r.secret, ...authFrame(st) } });
|
|
1328
1419
|
} else {
|
|
@@ -1351,7 +1442,7 @@ function createEngine(deps) {
|
|
|
1351
1442
|
const scheduleReconnect = (st, r) => {
|
|
1352
1443
|
if (st.closed || r.reconnectTimer)
|
|
1353
1444
|
return;
|
|
1354
|
-
const wait = r.backoff;
|
|
1445
|
+
const wait = Math.max(250, Math.round(r.backoff * (0.8 + Math.random() * 0.4)));
|
|
1355
1446
|
r.backoff = Math.min(r.backoff * 2, 60000);
|
|
1356
1447
|
r.reconnectTimer = setTimeout(() => {
|
|
1357
1448
|
r.reconnectTimer = null;
|
|
@@ -1433,6 +1524,11 @@ function createEngine(deps) {
|
|
|
1433
1524
|
}
|
|
1434
1525
|
};
|
|
1435
1526
|
const handleFrame = async (st, r, frame) => {
|
|
1527
|
+
if (frame.op === "authed") {
|
|
1528
|
+
r.authWaiter?.();
|
|
1529
|
+
r.authWaiter = undefined;
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1436
1532
|
if (frame.op === "init") {
|
|
1437
1533
|
if (!frame.ok) {
|
|
1438
1534
|
const err = String(frame.error ?? "");
|
|
@@ -1471,6 +1567,16 @@ function createEngine(deps) {
|
|
|
1471
1567
|
queueConnect(st, r);
|
|
1472
1568
|
return;
|
|
1473
1569
|
}
|
|
1570
|
+
if (/limit|too many|rate/i.test(err)) {
|
|
1571
|
+
logError(`${r.url}: create refused:`, frame.error, "- retrying with backoff");
|
|
1572
|
+
r.connecting = false;
|
|
1573
|
+
try {
|
|
1574
|
+
r.ws?.close(4000, "rate limited");
|
|
1575
|
+
} catch {}
|
|
1576
|
+
if (!st.closed)
|
|
1577
|
+
scheduleReconnect(st, r);
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1474
1580
|
logError(`${r.url}: init refused:`, frame.error, "- creating fresh share");
|
|
1475
1581
|
deleteAbandoned(r);
|
|
1476
1582
|
r.shareId = null;
|
|
@@ -1499,11 +1605,19 @@ function createEngine(deps) {
|
|
|
1499
1605
|
r.connecting = false;
|
|
1500
1606
|
r.initWasAttach = false;
|
|
1501
1607
|
r.collided = null;
|
|
1608
|
+
if (r.frozenRemote && !frame.error)
|
|
1609
|
+
r.frozenRemote = false;
|
|
1502
1610
|
if (r.initTimer) {
|
|
1503
1611
|
clearTimeout(r.initTimer);
|
|
1504
1612
|
r.initTimer = null;
|
|
1505
1613
|
}
|
|
1506
|
-
r.
|
|
1614
|
+
if (r.backoffResetTimer)
|
|
1615
|
+
clearTimeout(r.backoffResetTimer);
|
|
1616
|
+
r.backoffResetTimer = setTimeout(() => {
|
|
1617
|
+
r.backoffResetTimer = null;
|
|
1618
|
+
if (r.wsReady && !r.reconnectTimer)
|
|
1619
|
+
r.backoff = 1000;
|
|
1620
|
+
}, 30000);
|
|
1507
1621
|
st.everConnected = true;
|
|
1508
1622
|
if (frame.policy)
|
|
1509
1623
|
applyPolicy(frame.policy, r.url);
|
|
@@ -1523,18 +1637,49 @@ function createEngine(deps) {
|
|
|
1523
1637
|
flush(st);
|
|
1524
1638
|
}
|
|
1525
1639
|
if (wasFanoutCreate) {
|
|
1640
|
+
st.sentHash.clear();
|
|
1526
1641
|
chain = chain.then(() => backfill(st)).catch((e) => logError("fan-out backfill error", e?.message || e));
|
|
1527
1642
|
}
|
|
1528
1643
|
return;
|
|
1529
1644
|
}
|
|
1530
1645
|
if (frame.op === "ack") {
|
|
1531
1646
|
r.ackedRev = frame.rev ?? r.ackedRev;
|
|
1532
|
-
r.
|
|
1533
|
-
|
|
1647
|
+
if (r.backoffResetTimer) {
|
|
1648
|
+
clearTimeout(r.backoffResetTimer);
|
|
1649
|
+
r.backoffResetTimer = null;
|
|
1650
|
+
r.backoff = 1000;
|
|
1651
|
+
}
|
|
1652
|
+
if (typeof frame.seq === "number") {
|
|
1653
|
+
r.pending = r.pending.filter((p) => p.seq !== frame.seq);
|
|
1654
|
+
} else {
|
|
1655
|
+
r.pending = [];
|
|
1656
|
+
}
|
|
1657
|
+
scheduleSpoolSave(st, r);
|
|
1534
1658
|
scheduleBindingWrite(st);
|
|
1535
1659
|
return;
|
|
1536
1660
|
}
|
|
1537
1661
|
if (frame.op === "nack" || frame.op === "error") {
|
|
1662
|
+
const errText = String(frame.error ?? "");
|
|
1663
|
+
if (frame.op === "nack") {
|
|
1664
|
+
if (/not found|gone/i.test(errText)) {
|
|
1665
|
+
logError(`${r.url}: share vanished server-side (nack: ${errText}) - recreating`);
|
|
1666
|
+
deleteAbandoned(r);
|
|
1667
|
+
r.shareId = null;
|
|
1668
|
+
r.secret = null;
|
|
1669
|
+
r.ackedRev = 0;
|
|
1670
|
+
try {
|
|
1671
|
+
r.ws?.close(4000, "share gone");
|
|
1672
|
+
} catch {}
|
|
1673
|
+
if (!st.closed)
|
|
1674
|
+
queueConnect(st, r);
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
if (/frozen/i.test(errText)) {
|
|
1678
|
+
r.frozenRemote = true;
|
|
1679
|
+
logError(`${r.url}: share frozen by the hoster - edits refused; contact the server admin`);
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1538
1683
|
logError(`${r.url}: worker refused frame:`, JSON.stringify(frame).slice(0, 200));
|
|
1539
1684
|
if (frame.op === "error" && r.wsReady && !st.closed) {
|
|
1540
1685
|
r.wsReady = false;
|
|
@@ -1557,7 +1702,7 @@ function createEngine(deps) {
|
|
|
1557
1702
|
queueConnect(st, r);
|
|
1558
1703
|
};
|
|
1559
1704
|
const frameFrom = (st) => {
|
|
1560
|
-
stampsPass();
|
|
1705
|
+
stampsPass(st);
|
|
1561
1706
|
const items = Array.from(st.queue.values());
|
|
1562
1707
|
st.queue.clear();
|
|
1563
1708
|
const statusChanged = st.status !== st.lastSentStatus;
|
|
@@ -1577,14 +1722,32 @@ function createEngine(deps) {
|
|
|
1577
1722
|
const f = frameFrom(st);
|
|
1578
1723
|
if (!f)
|
|
1579
1724
|
return;
|
|
1725
|
+
const chunks = f.items.length ? chunkedFrames(f.items, 200, 512 * 1024) : [];
|
|
1580
1726
|
let httpWork;
|
|
1581
1727
|
for (const r of st.remotes.values()) {
|
|
1582
|
-
r.
|
|
1583
|
-
|
|
1728
|
+
if (r.frozenRemote)
|
|
1729
|
+
continue;
|
|
1730
|
+
const fresh = [];
|
|
1731
|
+
if (chunks.length) {
|
|
1732
|
+
for (const c of chunks) {
|
|
1733
|
+
const seq = ++nextSeq;
|
|
1734
|
+
const cf = { items: c.items, status: f.status, seq, wire: syncFrameStr(c.json, f.status, seq) };
|
|
1735
|
+
fresh.push(cf);
|
|
1736
|
+
r.pending.push(cf);
|
|
1737
|
+
spoolAppend(st, r, cf);
|
|
1738
|
+
}
|
|
1739
|
+
} else {
|
|
1740
|
+
const seq = ++nextSeq;
|
|
1741
|
+
const cf = { items: [], status: f.status, seq };
|
|
1742
|
+
fresh.push(cf);
|
|
1743
|
+
r.pending.push(cf);
|
|
1744
|
+
spoolAppend(st, r, cf);
|
|
1745
|
+
}
|
|
1584
1746
|
if (r.ws && !r.wsReady)
|
|
1585
1747
|
continue;
|
|
1586
1748
|
if (r.wsReady) {
|
|
1587
|
-
|
|
1749
|
+
for (const cf of fresh)
|
|
1750
|
+
sendSync(r, cf);
|
|
1588
1751
|
continue;
|
|
1589
1752
|
}
|
|
1590
1753
|
const viaHttp = flushViaHttp(st, r);
|
|
@@ -1593,21 +1756,55 @@ function createEngine(deps) {
|
|
|
1593
1756
|
}
|
|
1594
1757
|
if (st.remotes.size)
|
|
1595
1758
|
st.lastFlushAt = Date.now();
|
|
1759
|
+
if (httpWork)
|
|
1760
|
+
httpWork.catch((e) => logError("http flush error", e?.message || e));
|
|
1596
1761
|
return httpWork;
|
|
1597
1762
|
};
|
|
1763
|
+
const httpDedupeKey = (it) => {
|
|
1764
|
+
if (!it || typeof it !== "object")
|
|
1765
|
+
return null;
|
|
1766
|
+
const o = it;
|
|
1767
|
+
switch (o.type) {
|
|
1768
|
+
case "session":
|
|
1769
|
+
return "session/" + (o.data?.id ?? "");
|
|
1770
|
+
case "session_diff":
|
|
1771
|
+
return "session_diff/" + (o.sessionID ?? o.data?.sessionID ?? "");
|
|
1772
|
+
case "message":
|
|
1773
|
+
return "message/" + (o.data?.id ?? "");
|
|
1774
|
+
case "part":
|
|
1775
|
+
return "part/" + (o.data?.messageID ?? "") + "/" + (o.data?.id ?? "");
|
|
1776
|
+
case "model":
|
|
1777
|
+
return "model/" + (o.sessionID ?? o.data?.sessionID ?? "");
|
|
1778
|
+
case "prefs":
|
|
1779
|
+
return "prefs";
|
|
1780
|
+
default:
|
|
1781
|
+
return null;
|
|
1782
|
+
}
|
|
1783
|
+
};
|
|
1598
1784
|
const flushViaHttp = async (st, r) => {
|
|
1599
1785
|
if (!r.shareId || !r.pending.length)
|
|
1600
1786
|
return;
|
|
1601
|
-
const
|
|
1787
|
+
const byKey = new Map;
|
|
1788
|
+
const unkeyed = [];
|
|
1602
1789
|
let status;
|
|
1790
|
+
const sentSeqs = new Set;
|
|
1603
1791
|
for (const f of r.pending) {
|
|
1604
|
-
for (const it of f.items)
|
|
1605
|
-
|
|
1792
|
+
for (const it of f.items) {
|
|
1793
|
+
const k = httpDedupeKey(it);
|
|
1794
|
+
if (k)
|
|
1795
|
+
byKey.set(k, it);
|
|
1796
|
+
else
|
|
1797
|
+
unkeyed.push(it);
|
|
1798
|
+
}
|
|
1606
1799
|
if (f.status)
|
|
1607
1800
|
status = f.status;
|
|
1801
|
+
if (typeof f.seq === "number")
|
|
1802
|
+
sentSeqs.add(f.seq);
|
|
1608
1803
|
}
|
|
1804
|
+
const items = [...byKey.values(), ...unkeyed];
|
|
1609
1805
|
const cred = credOf(r.url);
|
|
1610
|
-
|
|
1806
|
+
const chunks = items.length ? chunkItems(items, 500, 4 * 1024 * 1024) : [[]];
|
|
1807
|
+
for (const chunk of chunks) {
|
|
1611
1808
|
const res = await fetch(`${r.url}/api/share/${r.shareId}/sync`, {
|
|
1612
1809
|
method: "POST",
|
|
1613
1810
|
headers: { "content-type": "application/json", ...cred.credential ? { authorization: `Bearer ${cred.credential}` } : {} },
|
|
@@ -1619,8 +1816,8 @@ function createEngine(deps) {
|
|
|
1619
1816
|
const body = await res.json().catch(() => ({}));
|
|
1620
1817
|
r.ackedRev = body.rev ?? r.ackedRev;
|
|
1621
1818
|
}
|
|
1622
|
-
r.pending =
|
|
1623
|
-
|
|
1819
|
+
r.pending = r.pending.filter((p) => !sentSeqs.has(p.seq));
|
|
1820
|
+
scheduleSpoolSave(st, r);
|
|
1624
1821
|
};
|
|
1625
1822
|
const scheduleFlush = (st) => {
|
|
1626
1823
|
if (st.timer)
|
|
@@ -1631,12 +1828,27 @@ function createEngine(deps) {
|
|
|
1631
1828
|
st.timer = null;
|
|
1632
1829
|
if (debugEvents)
|
|
1633
1830
|
log(`flush timer fired sid=${st.sid.slice(0, 18)} queue=${st.queue.size} remotes=${st.remotes.size}`);
|
|
1634
|
-
chain = chain.then(() =>
|
|
1831
|
+
chain = chain.then(() => {
|
|
1832
|
+
flush(st);
|
|
1833
|
+
}).catch((e) => logError("flush error", e?.message || e));
|
|
1635
1834
|
}, wait);
|
|
1636
1835
|
};
|
|
1637
1836
|
const enqueue = (st, entry) => {
|
|
1638
1837
|
if (!st || !entry)
|
|
1639
1838
|
return;
|
|
1839
|
+
let h;
|
|
1840
|
+
try {
|
|
1841
|
+
h = createHash("sha256").update(JSON.stringify(entry.item)).digest("hex");
|
|
1842
|
+
} catch {
|
|
1843
|
+
h = "";
|
|
1844
|
+
}
|
|
1845
|
+
if (h && st.sentHash.get(entry.key) === h) {
|
|
1846
|
+
if (debugEvents)
|
|
1847
|
+
log(`enqueue dedupe ${entry.item.type} ${entry.key.slice(0, 40)}`);
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
if (h)
|
|
1851
|
+
st.sentHash.set(entry.key, h);
|
|
1640
1852
|
st.queue.set(entry.key, entry.item);
|
|
1641
1853
|
indexMessage(entry);
|
|
1642
1854
|
if (debugEvents)
|
|
@@ -1646,6 +1858,7 @@ function createEngine(deps) {
|
|
|
1646
1858
|
scheduleFlush(st);
|
|
1647
1859
|
};
|
|
1648
1860
|
const msgIndex = new Map;
|
|
1861
|
+
const tombstoned = new Set;
|
|
1649
1862
|
const indexMessage = (entry) => {
|
|
1650
1863
|
if (entry.item.type !== "message")
|
|
1651
1864
|
return;
|
|
@@ -1667,6 +1880,8 @@ function createEngine(deps) {
|
|
|
1667
1880
|
};
|
|
1668
1881
|
const sessionStateOf = (sid) => sessions.get(childToParent.get(sid) ?? sid);
|
|
1669
1882
|
const reEnqueueMessage = (m) => {
|
|
1883
|
+
if (tombstoned.has(`${m.sid}:${m.data.id}`))
|
|
1884
|
+
return;
|
|
1670
1885
|
const st = sessionStateOf(m.sid);
|
|
1671
1886
|
if (st)
|
|
1672
1887
|
enqueue(st, itemFor("message", m.data, m.sid));
|
|
@@ -1683,15 +1898,7 @@ function createEngine(deps) {
|
|
|
1683
1898
|
}
|
|
1684
1899
|
return;
|
|
1685
1900
|
};
|
|
1686
|
-
const stampAgentSwitches = () => {
|
|
1687
|
-
const bySession = new Map;
|
|
1688
|
-
for (const m of msgIndex.values()) {
|
|
1689
|
-
const list = bySession.get(m.sid);
|
|
1690
|
-
if (list)
|
|
1691
|
-
list.push(m);
|
|
1692
|
-
else
|
|
1693
|
-
bySession.set(m.sid, [m]);
|
|
1694
|
-
}
|
|
1901
|
+
const stampAgentSwitches = (bySession) => {
|
|
1695
1902
|
for (const [, list] of bySession) {
|
|
1696
1903
|
list.sort((a, b) => a.created - b.created);
|
|
1697
1904
|
const users = list.map((m, i) => m.role === "user" ? i : -1).filter((i) => i >= 0);
|
|
@@ -1699,13 +1906,14 @@ function createEngine(deps) {
|
|
|
1699
1906
|
for (let k = 0;k < users.length; k++) {
|
|
1700
1907
|
const agent = segmentAgent(list, users[k]);
|
|
1701
1908
|
const d = list[users[k]].data;
|
|
1702
|
-
const meta = d.metadata ??= {};
|
|
1703
1909
|
const want = k > 0 && agent && prev && agent !== prev ? agent : undefined;
|
|
1704
|
-
if (want !==
|
|
1705
|
-
if (want)
|
|
1910
|
+
if (want !== d.metadata?.agentSwitch) {
|
|
1911
|
+
if (want) {
|
|
1912
|
+
const meta = d.metadata ??= {};
|
|
1706
1913
|
meta.agentSwitch = want;
|
|
1707
|
-
else
|
|
1708
|
-
delete
|
|
1914
|
+
} else if (d.metadata) {
|
|
1915
|
+
delete d.metadata.agentSwitch;
|
|
1916
|
+
}
|
|
1709
1917
|
reEnqueueMessage(list[users[k]]);
|
|
1710
1918
|
}
|
|
1711
1919
|
if (agent)
|
|
@@ -1713,15 +1921,7 @@ function createEngine(deps) {
|
|
|
1713
1921
|
}
|
|
1714
1922
|
}
|
|
1715
1923
|
};
|
|
1716
|
-
const stampTaskAgents = () => {
|
|
1717
|
-
const bySession = new Map;
|
|
1718
|
-
for (const m of msgIndex.values()) {
|
|
1719
|
-
const list = bySession.get(m.sid);
|
|
1720
|
-
if (list)
|
|
1721
|
-
list.push(m);
|
|
1722
|
-
else
|
|
1723
|
-
bySession.set(m.sid, [m]);
|
|
1724
|
-
}
|
|
1924
|
+
const stampTaskAgents = (bySession) => {
|
|
1725
1925
|
for (const list of bySession.values())
|
|
1726
1926
|
list.sort((a, b) => a.created - b.created);
|
|
1727
1927
|
for (const m of msgIndex.values()) {
|
|
@@ -1853,9 +2053,28 @@ function createEngine(deps) {
|
|
|
1853
2053
|
reEnqueueMessage(m);
|
|
1854
2054
|
}
|
|
1855
2055
|
};
|
|
1856
|
-
const stampsPass = () => {
|
|
1857
|
-
|
|
1858
|
-
|
|
2056
|
+
const stampsPass = (st) => {
|
|
2057
|
+
let any = false;
|
|
2058
|
+
for (const it of st.queue.values()) {
|
|
2059
|
+
if (it.type === "message") {
|
|
2060
|
+
any = true;
|
|
2061
|
+
break;
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
if (!any)
|
|
2065
|
+
return;
|
|
2066
|
+
const bySession = new Map;
|
|
2067
|
+
for (const m of msgIndex.values()) {
|
|
2068
|
+
const list = bySession.get(m.sid);
|
|
2069
|
+
if (list)
|
|
2070
|
+
list.push(m);
|
|
2071
|
+
else
|
|
2072
|
+
bySession.set(m.sid, [m]);
|
|
2073
|
+
}
|
|
2074
|
+
for (const list of bySession.values())
|
|
2075
|
+
list.sort((a, b) => a.created - b.created);
|
|
2076
|
+
stampAgentSwitches(bySession);
|
|
2077
|
+
stampTaskAgents(bySession);
|
|
1859
2078
|
stampFileLanguages();
|
|
1860
2079
|
};
|
|
1861
2080
|
const mergeEntry = (st, messageID) => {
|
|
@@ -1905,8 +2124,11 @@ function createEngine(deps) {
|
|
|
1905
2124
|
e.parts.set(part.id, part);
|
|
1906
2125
|
emitMerged(st, sid, part.messageID);
|
|
1907
2126
|
};
|
|
1908
|
-
const dropMergedMessage = (st, messageID) => {
|
|
2127
|
+
const dropMergedMessage = (st, messageID, evSid) => {
|
|
2128
|
+
const sid = evSid ?? st.sid;
|
|
1909
2129
|
st.merged.delete(messageID);
|
|
2130
|
+
msgIndex.delete(`${sid}:${messageID}`);
|
|
2131
|
+
tombstoned.add(`${sid}:${messageID}`);
|
|
1910
2132
|
};
|
|
1911
2133
|
const dropMergedPart = (st, sid, messageID, partID) => {
|
|
1912
2134
|
const e = st.merged.get(messageID);
|
|
@@ -1934,6 +2156,10 @@ function createEngine(deps) {
|
|
|
1934
2156
|
clearTimeout(r.initTimer);
|
|
1935
2157
|
r.initTimer = null;
|
|
1936
2158
|
}
|
|
2159
|
+
if (r.backoffResetTimer) {
|
|
2160
|
+
clearTimeout(r.backoffResetTimer);
|
|
2161
|
+
r.backoffResetTimer = null;
|
|
2162
|
+
}
|
|
1937
2163
|
if (r.reconnectTimer) {
|
|
1938
2164
|
clearTimeout(r.reconnectTimer);
|
|
1939
2165
|
r.reconnectTimer = null;
|
|
@@ -1943,9 +2169,21 @@ function createEngine(deps) {
|
|
|
1943
2169
|
clearTimeout(r.idleDropTimer);
|
|
1944
2170
|
r.idleDropTimer = null;
|
|
1945
2171
|
}
|
|
1946
|
-
if (r.wsReady)
|
|
2172
|
+
if (r.wsReady && r.shareId) {
|
|
1947
2173
|
wsSend(r, { op: "delete" });
|
|
1948
|
-
|
|
2174
|
+
if (r.shareId && r.secret) {
|
|
2175
|
+
const sid = r.shareId;
|
|
2176
|
+
const sec = r.secret;
|
|
2177
|
+
setTimeout(() => {
|
|
2178
|
+
fetch(`${r.url}/api/share/${sid}`, {
|
|
2179
|
+
method: "DELETE",
|
|
2180
|
+
headers: { "content-type": "application/json" },
|
|
2181
|
+
body: JSON.stringify({ secret: sec }),
|
|
2182
|
+
signal: AbortSignal.timeout(8000)
|
|
2183
|
+
}).catch(() => {});
|
|
2184
|
+
}, 2000);
|
|
2185
|
+
}
|
|
2186
|
+
} else if (r.shareId && r.secret) {
|
|
1949
2187
|
fetch(`${r.url}/api/share/${r.shareId}`, {
|
|
1950
2188
|
method: "DELETE",
|
|
1951
2189
|
headers: { "content-type": "application/json" },
|
|
@@ -1963,6 +2201,17 @@ function createEngine(deps) {
|
|
|
1963
2201
|
}, 1000);
|
|
1964
2202
|
for (const r of st.remotes.values())
|
|
1965
2203
|
spoolClear(st, r);
|
|
2204
|
+
const stSid = st.sid;
|
|
2205
|
+
const pfx = `${stSid}:`;
|
|
2206
|
+
for (const k of msgIndex.keys())
|
|
2207
|
+
if (k.startsWith(pfx))
|
|
2208
|
+
msgIndex.delete(k);
|
|
2209
|
+
for (const k of tombstoned)
|
|
2210
|
+
if (k.startsWith(pfx))
|
|
2211
|
+
tombstoned.delete(k);
|
|
2212
|
+
for (const [child, parent] of childToParent)
|
|
2213
|
+
if (parent === stSid || child === stSid)
|
|
2214
|
+
childToParent.delete(child);
|
|
1966
2215
|
sessions.delete(st.sid);
|
|
1967
2216
|
try {
|
|
1968
2217
|
chain = chain.then(async () => {
|
|
@@ -2022,6 +2271,9 @@ function createEngine(deps) {
|
|
|
2022
2271
|
break;
|
|
2023
2272
|
before = nextBefore;
|
|
2024
2273
|
}
|
|
2274
|
+
if (out.length >= MAX_PAGES * PAGE_SIZE) {
|
|
2275
|
+
logError(`backfill truncated at ${out.length} messages (page cap) for ${sid.slice(0, 18)} — oldest history not shared`);
|
|
2276
|
+
}
|
|
2025
2277
|
return out;
|
|
2026
2278
|
}
|
|
2027
2279
|
const backfillSession = async (st, client, sid, info) => {
|
|
@@ -2266,7 +2518,9 @@ function createEngine(deps) {
|
|
|
2266
2518
|
if (st.timer)
|
|
2267
2519
|
clearTimeout(st.timer);
|
|
2268
2520
|
st.timer = null;
|
|
2269
|
-
chain = chain.then(() =>
|
|
2521
|
+
chain = chain.then(() => {
|
|
2522
|
+
flush(st);
|
|
2523
|
+
}).catch((e) => logError("idle flush error", e?.message || e));
|
|
2270
2524
|
const epoch = st.statusEpoch;
|
|
2271
2525
|
const settle = () => {
|
|
2272
2526
|
st.status = st.badSignal ? "error" : "completed";
|
|
@@ -2274,7 +2528,9 @@ function createEngine(deps) {
|
|
|
2274
2528
|
if (st.timer)
|
|
2275
2529
|
clearTimeout(st.timer);
|
|
2276
2530
|
st.timer = null;
|
|
2277
|
-
chain = chain.then(() =>
|
|
2531
|
+
chain = chain.then(() => {
|
|
2532
|
+
flush(st);
|
|
2533
|
+
}).then(() => {
|
|
2278
2534
|
armSettleDrop(st);
|
|
2279
2535
|
}).catch((e) => logError("settle flush error", e?.message || e));
|
|
2280
2536
|
};
|
|
@@ -2305,8 +2561,8 @@ function createEngine(deps) {
|
|
|
2305
2561
|
if (!messageID)
|
|
2306
2562
|
return;
|
|
2307
2563
|
if (t === "message.removed") {
|
|
2308
|
-
dropMergedMessage(st, messageID);
|
|
2309
|
-
enqueue(st, { key: `message/${messageID}`, item: messageTombstone(
|
|
2564
|
+
dropMergedMessage(st, messageID, sid);
|
|
2565
|
+
enqueue(st, { key: `message/${messageID}`, item: messageTombstone(sid, messageID) });
|
|
2310
2566
|
} else {
|
|
2311
2567
|
const partID = p.partID;
|
|
2312
2568
|
if (!partID)
|
|
@@ -2417,6 +2673,13 @@ function createEngine(deps) {
|
|
|
2417
2673
|
await flushViaHttp(st, r).catch(() => {});
|
|
2418
2674
|
}
|
|
2419
2675
|
}
|
|
2676
|
+
for (const r of st.remotes.values()) {
|
|
2677
|
+
if (r.spoolSaveTimer) {
|
|
2678
|
+
clearTimeout(r.spoolSaveTimer);
|
|
2679
|
+
r.spoolSaveTimer = null;
|
|
2680
|
+
spoolSave(st, r);
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2420
2683
|
if (st.metaTimer)
|
|
2421
2684
|
clearTimeout(st.metaTimer);
|
|
2422
2685
|
st.metaTimer = null;
|
|
@@ -2569,6 +2832,7 @@ function createV2ServerSetup() {
|
|
|
2569
2832
|
return () => {};
|
|
2570
2833
|
}
|
|
2571
2834
|
const pendingRefresh = new Map;
|
|
2835
|
+
const queuedInbox = new Map;
|
|
2572
2836
|
const refresh = (sessionID) => {
|
|
2573
2837
|
let timer = pendingRefresh.get(sessionID);
|
|
2574
2838
|
if (timer)
|
|
@@ -2583,7 +2847,10 @@ function createV2ServerSetup() {
|
|
|
2583
2847
|
});
|
|
2584
2848
|
const messages = await client.session.messages?.({ path: { id: sessionID } });
|
|
2585
2849
|
const list = messages?.data ?? [];
|
|
2850
|
+
const held = queuedInbox.get(sessionID);
|
|
2586
2851
|
for (const message of list) {
|
|
2852
|
+
if (held?.has(message.info.id))
|
|
2853
|
+
continue;
|
|
2587
2854
|
await engine.hooks.event({
|
|
2588
2855
|
event: { type: "message.updated", properties: { sessionID, info: message.info } }
|
|
2589
2856
|
});
|
|
@@ -2616,6 +2883,33 @@ function createV2ServerSetup() {
|
|
|
2616
2883
|
await engine.hooks.event({ event: { type, properties: { sessionID: sid } } });
|
|
2617
2884
|
continue;
|
|
2618
2885
|
}
|
|
2886
|
+
if (type.startsWith("session.inbox.")) {
|
|
2887
|
+
const data = event.data ?? {};
|
|
2888
|
+
const inboxID = data.inboxID;
|
|
2889
|
+
if (inboxID) {
|
|
2890
|
+
let set = queuedInbox.get(sid);
|
|
2891
|
+
if (!set) {
|
|
2892
|
+
set = new Set;
|
|
2893
|
+
queuedInbox.set(sid, set);
|
|
2894
|
+
}
|
|
2895
|
+
if (type === "session.inbox.enqueued" && data.item?.delivery === "queue") {
|
|
2896
|
+
set.add(inboxID);
|
|
2897
|
+
} else if (type === "session.inbox.delivered") {
|
|
2898
|
+
set.delete(inboxID);
|
|
2899
|
+
refresh(sid);
|
|
2900
|
+
} else if (type === "session.inbox.cancelled") {
|
|
2901
|
+
set.add(inboxID);
|
|
2902
|
+
} else if (type === "session.inbox.delivery.changed") {
|
|
2903
|
+
if (data.item?.delivery === "queue")
|
|
2904
|
+
set.add(inboxID);
|
|
2905
|
+
else {
|
|
2906
|
+
set.delete(inboxID);
|
|
2907
|
+
refresh(sid);
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
continue;
|
|
2912
|
+
}
|
|
2619
2913
|
if (type === "message.removed" || type === "message.part.removed") {
|
|
2620
2914
|
const data = event.data ?? {};
|
|
2621
2915
|
await engine.hooks.event({
|