@cryptiklemur/lattice 5.4.3 → 5.6.0
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 +114 -0
- package/dist/client/assets/{angular-html-BMCIs4B9.js → angular-html-B_LrO4d_.js} +1 -1
- package/dist/client/assets/{angular-ts-Dl9U67Xi.js → angular-ts-DG2-AEn3.js} +1 -1
- package/dist/client/assets/{apl-Dc2Jp1EP.js → apl-D9AV-2KJ.js} +1 -1
- package/dist/client/assets/{astro-KOvSEh6g.js → astro-D55z-bYO.js} +1 -1
- package/dist/client/assets/{blade-Bp9mbJ5K.js → blade-Bs-tcMis.js} +1 -1
- package/dist/client/assets/{c-C0ETB1bm.js → c-CAs-oCwZ.js} +1 -1
- package/dist/client/assets/{cobol-DE5VmQJX.js → cobol-0B2WKsi0.js} +1 -1
- package/dist/client/assets/{coffee-COzJhfRU.js → coffee-BLi0y75x.js} +1 -1
- package/dist/client/assets/{cpp-jzhs79Ii.js → cpp-CTXpwK-o.js} +1 -1
- package/dist/client/assets/{crystal-Czf3Etrk.js → crystal-B_OPrNIv.js} +1 -1
- package/dist/client/assets/{css-Cf45eU4M.js → css-C9nlz4Z2.js} +1 -1
- package/dist/client/assets/{dist-Bk-EVR9B.js → dist-CzUlrFrL.js} +2 -2
- package/dist/client/assets/{edge-BmKSsYuX.js → edge-DE3SfmPJ.js} +1 -1
- package/dist/client/assets/{elixir-Ds7FEpTU.js → elixir-BS_1fIPf.js} +1 -1
- package/dist/client/assets/{elm-C_AmF7eK.js → elm-CrfkxZpb.js} +1 -1
- package/dist/client/assets/{erb-DevlC2ei.js → erb-B6Ab2Y07.js} +1 -1
- package/dist/client/assets/{git-rebase-CpJCeeHA.js → git-rebase-RkfMNUtm.js} +1 -1
- package/dist/client/assets/{glimmer-js-CFH-Kzxa.js → glimmer-js-CBA96B3V.js} +1 -1
- package/dist/client/assets/{glimmer-ts-BkiQ1tjj.js → glimmer-ts-WPYNrOak.js} +1 -1
- package/dist/client/assets/{glsl-DLuvkGQM.js → glsl-BVPbkPwC.js} +1 -1
- package/dist/client/assets/{graphql-CYjDXA-l.js → graphql-CplPyf-J.js} +1 -1
- package/dist/client/assets/{hack-CDfrJ_Sc.js → hack-Bqnr1D44.js} +1 -1
- package/dist/client/assets/{haml-C4YjrCNF.js → haml-Bk9H5oqC.js} +1 -1
- package/dist/client/assets/{handlebars-WIT13u08.js → handlebars-Dho-7JSJ.js} +1 -1
- package/dist/client/assets/{html-ntqTOVvn.js → html-Dx1dAJ5K.js} +1 -1
- package/dist/client/assets/{html-derivative-B3rc20Rp.js → html-derivative-DMEkbacz.js} +1 -1
- package/dist/client/assets/{http-jTU2ko3B.js → http-yzZRblhh.js} +1 -1
- package/dist/client/assets/{hurl-D2HM7b2B.js → hurl-DwA-puXE.js} +1 -1
- package/dist/client/assets/{index-CZNra7P3.js → index-BV9isded.js} +321 -88
- package/dist/client/assets/{index-DCKRhnC1.css → index-DcwXjiH0.css} +1 -1
- package/dist/client/assets/{java-BofMW2-d.js → java-da4cNL_S.js} +1 -1
- package/dist/client/assets/{javascript-DHlF2jGd.js → javascript-BRBtsTGZ.js} +1 -1
- package/dist/client/assets/{jinja-BXkUuA_p.js → jinja-KYkTqvLF.js} +1 -1
- package/dist/client/assets/{jison-CYSiwIU-.js → jison-CpJZ2qu3.js} +1 -1
- package/dist/client/assets/{json-5cpQWmWq.js → json-1ZKXsOcj.js} +1 -1
- package/dist/client/assets/{jsx-D8kfeKVb.js → jsx-Ci3IutCK.js} +1 -1
- package/dist/client/assets/{julia-BkW_r2Xd.js → julia-8BIB33ff.js} +1 -1
- package/dist/client/assets/{just-Dz61xACw.js → just-BlxIBqAl.js} +1 -1
- package/dist/client/assets/{latex-gqtZNh_M.js → latex-Dt6aIDIN.js} +1 -1
- package/dist/client/assets/{liquid-KcpeevnQ.js → liquid-rNU0f2fO.js} +1 -1
- package/dist/client/assets/{lua-C3Vw3Kyi.js → lua-9O59duWh.js} +1 -1
- package/dist/client/assets/{marko-DVaxNS7b.js → marko-cSK6Q4M9.js} +1 -1
- package/dist/client/assets/{mdc-DRm1pTBY.js → mdc-DD0DONjK.js} +1 -1
- package/dist/client/assets/{nginx-DYudX1m0.js → nginx-Bfzt7qPl.js} +1 -1
- package/dist/client/assets/{nim-DejMEf9z.js → nim-DOD72OhS.js} +1 -1
- package/dist/client/assets/{perl-D09qXnjW.js → perl-1cg42R6O.js} +1 -1
- package/dist/client/assets/{php-BrfQzrDQ.js → php-DfG0eJE0.js} +1 -1
- package/dist/client/assets/{pug-DPbk8XD2.js → pug-BJ4ktrjh.js} +1 -1
- package/dist/client/assets/{qml-BCh8MHc-.js → qml-BwGhenQ4.js} +1 -1
- package/dist/client/assets/{r-DCcyjvCV.js → r-DSKy_gZ9.js} +1 -1
- package/dist/client/assets/{razor-CHeljxcn.js → razor-morGoSMw.js} +1 -1
- package/dist/client/assets/{regexp-DpLv_uhV.js → regexp-DSQzd7Gw.js} +1 -1
- package/dist/client/assets/{rst-BmAGX4li.js → rst-CQ4DB61w.js} +1 -1
- package/dist/client/assets/{ruby-C28IDakj.js → ruby-dcUoNsdX.js} +1 -1
- package/dist/client/assets/{sas-Dq78Tt13.js → sas-BckOIwmR.js} +1 -1
- package/dist/client/assets/{scss-CCq8md_z.js → scss-CIDQomQQ.js} +1 -1
- package/dist/client/assets/{shellscript-Cl_slHLv.js → shellscript-DscXCB_p.js} +1 -1
- package/dist/client/assets/{shellsession-D50FTgCo.js → shellsession-C9EjZURj.js} +1 -1
- package/dist/client/assets/{soy-Da12pRro.js → soy-CVzyKHIE.js} +1 -1
- package/dist/client/assets/{sql-DRKhJcrs.js → sql-Ddk-vRG3.js} +1 -1
- package/dist/client/assets/{stata-E06HAz87.js → stata-BGl5cxb8.js} +1 -1
- package/dist/client/assets/{surrealql-CTaVrdiv.js → surrealql-3IZgL_Ze.js} +1 -1
- package/dist/client/assets/{svelte-j0H5d0BS.js → svelte-BRM0niF5.js} +1 -1
- package/dist/client/assets/{templ-BXfOHYWo.js → templ-qbgb30u-.js} +1 -1
- package/dist/client/assets/{tex-CGBWhBxV.js → tex-Ct_-hpJo.js} +1 -1
- package/dist/client/assets/{ts-tags-C-diQc0f.js → ts-tags-C85B12Rc.js} +1 -1
- package/dist/client/assets/{tsx-CUBPow91.js → tsx-d401eVG9.js} +1 -1
- package/dist/client/assets/{twig-BYoQufij.js → twig-CEvQ_MDP.js} +1 -1
- package/dist/client/assets/{typescript-CAiUPCi4.js → typescript-y2NNcyKH.js} +1 -1
- package/dist/client/assets/{vue-CEKqQRtx.js → vue-CiTZbygT.js} +1 -1
- package/dist/client/assets/{vue-html-DSRnbzbs.js → vue-html-B4nYUWKJ.js} +1 -1
- package/dist/client/assets/{vue-vine-PFaCZbOl.js → vue-vine-DdfbYtGV.js} +1 -1
- package/dist/client/assets/{xml-CpFeNxWK.js → xml-1B2HtnyO.js} +1 -1
- package/dist/client/assets/{xsl-DOlnqqgg.js → xsl-DoRYSDxo.js} +1 -1
- package/dist/client/assets/{yaml-Bv-3IQB_.js → yaml-DpbRFfH9.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/client/sw.js +1 -1
- package/dist/server/daemon.js +75 -26
- package/dist/server/features/brainstorm.js +267 -0
- package/dist/server/handlers/attachment.js +66 -29
- package/dist/server/handlers/brainstorm.js +33 -0
- package/dist/server/handlers/fs.js +61 -72
- package/dist/server/handlers/memory.js +37 -52
- package/dist/server/handlers/mesh.js +4 -1
- package/dist/server/handlers/session.js +47 -58
- package/dist/server/mesh/connector.js +85 -1
- package/dist/server/project/file-browser.js +14 -14
- package/dist/server/project/session.js +98 -14
- package/dist/server/ws/broadcast.js +60 -0
- package/package.json +1 -1
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, watch } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { loadConfig } from "../config.js";
|
|
4
|
+
import { broadcast } from "../ws/broadcast.js";
|
|
5
|
+
import { log } from "../logger.js";
|
|
6
|
+
var activeBrainstorms = {};
|
|
7
|
+
var watchers = [];
|
|
8
|
+
var debounceTimers = {};
|
|
9
|
+
function debounce(key, fn, ms) {
|
|
10
|
+
if (debounceTimers[key]) {
|
|
11
|
+
clearTimeout(debounceTimers[key]);
|
|
12
|
+
}
|
|
13
|
+
debounceTimers[key] = setTimeout(function () {
|
|
14
|
+
delete debounceTimers[key];
|
|
15
|
+
fn();
|
|
16
|
+
}, ms);
|
|
17
|
+
}
|
|
18
|
+
function getMostRecentSessionDir(brainstormDir) {
|
|
19
|
+
if (!existsSync(brainstormDir)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
var entries = [];
|
|
23
|
+
try {
|
|
24
|
+
var names = readdirSync(brainstormDir);
|
|
25
|
+
for (var i = 0; i < names.length; i++) {
|
|
26
|
+
var fullPath = join(brainstormDir, names[i]);
|
|
27
|
+
try {
|
|
28
|
+
var stat = statSync(fullPath);
|
|
29
|
+
if (stat.isDirectory()) {
|
|
30
|
+
entries.push({ name: names[i], mtime: stat.mtimeMs });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// skip
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
if (entries.length === 0) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
entries.sort(function (a, b) {
|
|
45
|
+
return b.mtime - a.mtime;
|
|
46
|
+
});
|
|
47
|
+
return join(brainstormDir, entries[0].name);
|
|
48
|
+
}
|
|
49
|
+
function isWaitingFile(filename) {
|
|
50
|
+
return filename === "waiting.html" || filename.startsWith("waiting");
|
|
51
|
+
}
|
|
52
|
+
function handleContentFile(projectSlug, contentDir, sessionDir, filename) {
|
|
53
|
+
if (!filename.endsWith(".html")) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (isWaitingFile(filename)) {
|
|
57
|
+
log.server("[brainstorm] waiting file detected for project %s", projectSlug);
|
|
58
|
+
delete activeBrainstorms[projectSlug];
|
|
59
|
+
broadcast({ type: "brainstorm:cleared" });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
var filePath = join(contentDir, filename);
|
|
63
|
+
if (!existsSync(filePath)) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
var html;
|
|
67
|
+
try {
|
|
68
|
+
html = readFileSync(filePath, "utf-8");
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
log.server("[brainstorm] failed to read %s: %s", filePath, err);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
log.server("[brainstorm] broadcasting content %s for project %s", filename, projectSlug);
|
|
75
|
+
activeBrainstorms[projectSlug] = { html, filename, sessionDir };
|
|
76
|
+
broadcast({ type: "brainstorm:content", html, filename, sessionDir });
|
|
77
|
+
}
|
|
78
|
+
function handleStateFile(projectSlug, filename) {
|
|
79
|
+
if (filename === "server-stopped") {
|
|
80
|
+
log.server("[brainstorm] server-stopped detected for project %s", projectSlug);
|
|
81
|
+
delete activeBrainstorms[projectSlug];
|
|
82
|
+
broadcast({ type: "brainstorm:cleared" });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function watchSessionDir(projectSlug, sessionDir) {
|
|
86
|
+
var contentDir = join(sessionDir, "content");
|
|
87
|
+
var stateDir = join(sessionDir, "state");
|
|
88
|
+
if (!existsSync(contentDir)) {
|
|
89
|
+
mkdirSync(contentDir, { recursive: true });
|
|
90
|
+
}
|
|
91
|
+
if (!existsSync(stateDir)) {
|
|
92
|
+
mkdirSync(stateDir, { recursive: true });
|
|
93
|
+
}
|
|
94
|
+
var existingFiles = [];
|
|
95
|
+
try {
|
|
96
|
+
existingFiles = readdirSync(contentDir);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// ok
|
|
100
|
+
}
|
|
101
|
+
for (var i = 0; i < existingFiles.length; i++) {
|
|
102
|
+
var fname = existingFiles[i];
|
|
103
|
+
if (fname.endsWith(".html") && !isWaitingFile(fname)) {
|
|
104
|
+
handleContentFile(projectSlug, contentDir, sessionDir, fname);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
var contentWatcher = watch(contentDir, function (eventType, filename) {
|
|
109
|
+
if (!filename) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
debounce("content:" + sessionDir + ":" + filename, function () {
|
|
113
|
+
handleContentFile(projectSlug, contentDir, sessionDir, filename);
|
|
114
|
+
}, 50);
|
|
115
|
+
});
|
|
116
|
+
watchers.push(contentWatcher);
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
log.server("[brainstorm] failed to watch content dir %s: %s", contentDir, err);
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
var stateWatcher = watch(stateDir, function (eventType, filename) {
|
|
123
|
+
if (!filename) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
debounce("state:" + sessionDir + ":" + filename, function () {
|
|
127
|
+
handleStateFile(projectSlug, filename);
|
|
128
|
+
}, 50);
|
|
129
|
+
});
|
|
130
|
+
watchers.push(stateWatcher);
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
log.server("[brainstorm] failed to watch state dir %s: %s", stateDir, err);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function watchBrainstormDir(projectSlug, brainstormDir) {
|
|
137
|
+
var recentSession = getMostRecentSessionDir(brainstormDir);
|
|
138
|
+
if (recentSession) {
|
|
139
|
+
watchSessionDir(projectSlug, recentSession);
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
var dirWatcher = watch(brainstormDir, function (eventType, filename) {
|
|
143
|
+
if (!filename) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
debounce("brainstorm:" + brainstormDir + ":" + filename, function () {
|
|
147
|
+
var candidate = join(brainstormDir, filename);
|
|
148
|
+
try {
|
|
149
|
+
var stat = statSync(candidate);
|
|
150
|
+
if (stat.isDirectory()) {
|
|
151
|
+
log.server("[brainstorm] new session dir detected: %s", candidate);
|
|
152
|
+
watchSessionDir(projectSlug, candidate);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// entry may have been removed
|
|
157
|
+
}
|
|
158
|
+
}, 50);
|
|
159
|
+
});
|
|
160
|
+
watchers.push(dirWatcher);
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
log.server("[brainstorm] failed to watch brainstorm dir %s: %s", brainstormDir, err);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function watchProjectRoot(projectSlug, projectPath) {
|
|
167
|
+
var superpowersDir = join(projectPath, ".superpowers");
|
|
168
|
+
var brainstormDir = join(superpowersDir, "brainstorm");
|
|
169
|
+
if (existsSync(brainstormDir)) {
|
|
170
|
+
watchBrainstormDir(projectSlug, brainstormDir);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (existsSync(superpowersDir)) {
|
|
174
|
+
watchForBrainstormDir(projectSlug, superpowersDir, brainstormDir);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
var rootWatcher = watch(projectPath, function (eventType, filename) {
|
|
179
|
+
if (filename !== ".superpowers") {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (existsSync(superpowersDir)) {
|
|
183
|
+
rootWatcher.close();
|
|
184
|
+
watchForBrainstormDir(projectSlug, superpowersDir, brainstormDir);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
watchers.push(rootWatcher);
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
log.server("[brainstorm] failed to watch project root %s: %s", projectPath, err);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function watchForBrainstormDir(projectSlug, superpowersDir, brainstormDir) {
|
|
194
|
+
if (existsSync(brainstormDir)) {
|
|
195
|
+
watchBrainstormDir(projectSlug, brainstormDir);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
var spWatcher = watch(superpowersDir, function (eventType, filename) {
|
|
200
|
+
if (filename !== "brainstorm") {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (existsSync(brainstormDir)) {
|
|
204
|
+
spWatcher.close();
|
|
205
|
+
watchBrainstormDir(projectSlug, brainstormDir);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
watchers.push(spWatcher);
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
log.server("[brainstorm] failed to watch .superpowers dir %s: %s", superpowersDir, err);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
export function startBrainstormWatchers() {
|
|
215
|
+
var config = loadConfig();
|
|
216
|
+
var projects = config.projects;
|
|
217
|
+
for (var i = 0; i < projects.length; i++) {
|
|
218
|
+
var project = projects[i];
|
|
219
|
+
if (!project.path || !project.slug) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (!existsSync(project.path)) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
log.server("[brainstorm] setting up watcher for project %s at %s", project.slug, project.path);
|
|
226
|
+
watchProjectRoot(project.slug, project.path);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
export function stopBrainstormWatchers() {
|
|
230
|
+
for (var i = 0; i < watchers.length; i++) {
|
|
231
|
+
try {
|
|
232
|
+
watchers[i].close();
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
// ignore
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
watchers = [];
|
|
239
|
+
activeBrainstorms = {};
|
|
240
|
+
var keys = Object.keys(debounceTimers);
|
|
241
|
+
for (var i = 0; i < keys.length; i++) {
|
|
242
|
+
clearTimeout(debounceTimers[keys[i]]);
|
|
243
|
+
}
|
|
244
|
+
debounceTimers = {};
|
|
245
|
+
}
|
|
246
|
+
export function getActiveBrainstorm(projectSlug) {
|
|
247
|
+
return activeBrainstorms[projectSlug] || null;
|
|
248
|
+
}
|
|
249
|
+
export function getAnyActiveBrainstorm() {
|
|
250
|
+
var keys = Object.keys(activeBrainstorms);
|
|
251
|
+
if (keys.length === 0)
|
|
252
|
+
return null;
|
|
253
|
+
return activeBrainstorms[keys[0]];
|
|
254
|
+
}
|
|
255
|
+
export function writeBrainstormEvent(sessionDir, event) {
|
|
256
|
+
var stateDir = join(sessionDir, "state");
|
|
257
|
+
if (!existsSync(stateDir)) {
|
|
258
|
+
mkdirSync(stateDir, { recursive: true });
|
|
259
|
+
}
|
|
260
|
+
var eventsFile = join(stateDir, "events");
|
|
261
|
+
try {
|
|
262
|
+
appendFileSync(eventsFile, JSON.stringify(event) + "\n", "utf-8");
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
log.server("[brainstorm] failed to write event to %s: %s", eventsFile, err);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import { createWriteStream, createReadStream } from "node:fs";
|
|
2
|
+
import { unlink, mkdtemp } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
1
5
|
import { registerHandler } from "../ws/router.js";
|
|
2
6
|
import { sendTo } from "../ws/broadcast.js";
|
|
7
|
+
import { log } from "../logger.js";
|
|
3
8
|
var MAX_ATTACHMENT_SIZE = 10 * 1024 * 1024;
|
|
4
9
|
var stores = new Map();
|
|
5
10
|
var completed = new Map();
|
|
@@ -21,22 +26,35 @@ function getClientCompleted(clientId) {
|
|
|
21
26
|
}
|
|
22
27
|
return store;
|
|
23
28
|
}
|
|
24
|
-
|
|
29
|
+
async function cleanupPending(pending) {
|
|
30
|
+
try {
|
|
31
|
+
pending.writeStream.destroy();
|
|
32
|
+
await unlink(pending.tempPath).catch(function () { });
|
|
33
|
+
await unlink(pending.tempDir).catch(function () { });
|
|
34
|
+
}
|
|
35
|
+
catch { }
|
|
36
|
+
}
|
|
37
|
+
registerHandler("attachment", async function (clientId, message) {
|
|
25
38
|
if (message.type === "attachment:chunk") {
|
|
26
39
|
var msg = message;
|
|
27
40
|
var store = getClientStore(clientId);
|
|
28
41
|
var pending = store.get(msg.attachmentId);
|
|
29
42
|
if (!pending) {
|
|
43
|
+
var tempDir = await mkdtemp(join(tmpdir(), "lattice-attach-"));
|
|
44
|
+
var tempPath = join(tempDir, "data.bin");
|
|
30
45
|
pending = {
|
|
31
|
-
|
|
46
|
+
tempDir: tempDir,
|
|
47
|
+
tempPath: tempPath,
|
|
48
|
+
writeStream: createWriteStream(tempPath),
|
|
32
49
|
totalChunks: msg.totalChunks,
|
|
33
50
|
receivedCount: 0,
|
|
34
51
|
totalBytes: 0,
|
|
35
52
|
createdAt: Date.now(),
|
|
53
|
+
chunksSeen: new Set(),
|
|
36
54
|
};
|
|
37
55
|
store.set(msg.attachmentId, pending);
|
|
38
56
|
}
|
|
39
|
-
if (pending.
|
|
57
|
+
if (pending.chunksSeen.has(msg.chunkIndex)) {
|
|
40
58
|
sendTo(clientId, {
|
|
41
59
|
type: "attachment:error",
|
|
42
60
|
attachmentId: msg.attachmentId,
|
|
@@ -46,6 +64,7 @@ registerHandler("attachment", function (clientId, message) {
|
|
|
46
64
|
}
|
|
47
65
|
var chunkBuffer = Buffer.from(msg.data, "base64");
|
|
48
66
|
if (pending.totalBytes + chunkBuffer.length > MAX_ATTACHMENT_SIZE) {
|
|
67
|
+
await cleanupPending(pending);
|
|
49
68
|
store.delete(msg.attachmentId);
|
|
50
69
|
sendTo(clientId, {
|
|
51
70
|
type: "attachment:error",
|
|
@@ -54,7 +73,8 @@ registerHandler("attachment", function (clientId, message) {
|
|
|
54
73
|
});
|
|
55
74
|
return;
|
|
56
75
|
}
|
|
57
|
-
pending.
|
|
76
|
+
pending.writeStream.write(chunkBuffer);
|
|
77
|
+
pending.chunksSeen.add(msg.chunkIndex);
|
|
58
78
|
pending.receivedCount++;
|
|
59
79
|
pending.totalBytes += chunkBuffer.length;
|
|
60
80
|
sendTo(clientId, {
|
|
@@ -85,36 +105,46 @@ registerHandler("attachment", function (clientId, message) {
|
|
|
85
105
|
});
|
|
86
106
|
return;
|
|
87
107
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
108
|
+
await new Promise(function (resolve) {
|
|
109
|
+
completePending.writeStream.end(function () { resolve(); });
|
|
110
|
+
});
|
|
111
|
+
try {
|
|
112
|
+
var assembled = await readTempFile(completePending.tempPath);
|
|
113
|
+
var isText = completeMsg.attachmentType === "paste" || isTextMimeType(completeMsg.mimeType);
|
|
114
|
+
var content = isText ? assembled.toString("utf-8") : assembled.toString("base64");
|
|
115
|
+
var attachment = {
|
|
116
|
+
type: completeMsg.attachmentType,
|
|
117
|
+
name: completeMsg.name,
|
|
118
|
+
content,
|
|
119
|
+
mimeType: completeMsg.mimeType,
|
|
120
|
+
size: completeMsg.size,
|
|
121
|
+
lineCount: completeMsg.lineCount,
|
|
122
|
+
};
|
|
123
|
+
var finishedStore = getClientCompleted(clientId);
|
|
124
|
+
finishedStore.set(completeMsg.attachmentId, attachment);
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
log.ws("Failed to read assembled attachment: %O", err);
|
|
128
|
+
sendTo(clientId, {
|
|
129
|
+
type: "attachment:error",
|
|
130
|
+
attachmentId: completeMsg.attachmentId,
|
|
131
|
+
error: "Failed to assemble attachment",
|
|
132
|
+
});
|
|
100
133
|
}
|
|
101
|
-
|
|
102
|
-
var isText = completeMsg.attachmentType === "paste" || isTextMimeType(completeMsg.mimeType);
|
|
103
|
-
var content = isText ? assembled.toString("utf-8") : assembled.toString("base64");
|
|
104
|
-
var attachment = {
|
|
105
|
-
type: completeMsg.attachmentType,
|
|
106
|
-
name: completeMsg.name,
|
|
107
|
-
content,
|
|
108
|
-
mimeType: completeMsg.mimeType,
|
|
109
|
-
size: completeMsg.size,
|
|
110
|
-
lineCount: completeMsg.lineCount,
|
|
111
|
-
};
|
|
112
|
-
var finishedStore = getClientCompleted(clientId);
|
|
113
|
-
finishedStore.set(completeMsg.attachmentId, attachment);
|
|
134
|
+
await cleanupPending(completePending);
|
|
114
135
|
completeStore.delete(completeMsg.attachmentId);
|
|
115
136
|
return;
|
|
116
137
|
}
|
|
117
138
|
});
|
|
139
|
+
function readTempFile(path) {
|
|
140
|
+
return new Promise(function (resolve, reject) {
|
|
141
|
+
var chunks = [];
|
|
142
|
+
var stream = createReadStream(path);
|
|
143
|
+
stream.on("data", function (chunk) { chunks.push(chunk); });
|
|
144
|
+
stream.on("end", function () { resolve(Buffer.concat(chunks)); });
|
|
145
|
+
stream.on("error", reject);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
118
148
|
function isTextMimeType(mime) {
|
|
119
149
|
if (mime.startsWith("text/"))
|
|
120
150
|
return true;
|
|
@@ -142,6 +172,12 @@ export function getAttachments(clientId, ids) {
|
|
|
142
172
|
return result;
|
|
143
173
|
}
|
|
144
174
|
export function cleanupClient(clientId) {
|
|
175
|
+
var clientStore = stores.get(clientId);
|
|
176
|
+
if (clientStore) {
|
|
177
|
+
for (var [, pending] of clientStore) {
|
|
178
|
+
void cleanupPending(pending);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
145
181
|
stores.delete(clientId);
|
|
146
182
|
completed.delete(clientId);
|
|
147
183
|
}
|
|
@@ -150,6 +186,7 @@ var ttlCleanupInterval = setInterval(function () {
|
|
|
150
186
|
stores.forEach(function (store) {
|
|
151
187
|
store.forEach(function (pending, id) {
|
|
152
188
|
if (now - pending.createdAt > TTL_MS) {
|
|
189
|
+
void cleanupPending(pending);
|
|
153
190
|
store.delete(id);
|
|
154
191
|
}
|
|
155
192
|
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { registerHandler } from "../ws/router.js";
|
|
2
|
+
import { sendTo } from "../ws/broadcast.js";
|
|
3
|
+
import { getActiveBrainstorm, getAnyActiveBrainstorm, writeBrainstormEvent } from "../features/brainstorm.js";
|
|
4
|
+
import { getActiveProjectForClient } from "./fs.js";
|
|
5
|
+
registerHandler("brainstorm", function (clientId, message) {
|
|
6
|
+
if (message.type === "brainstorm:select") {
|
|
7
|
+
var selectMsg = message;
|
|
8
|
+
writeBrainstormEvent(selectMsg.sessionDir, {
|
|
9
|
+
type: "click",
|
|
10
|
+
choice: selectMsg.choice,
|
|
11
|
+
text: selectMsg.text,
|
|
12
|
+
timestamp: Date.now(),
|
|
13
|
+
});
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (message.type === "brainstorm:status_request") {
|
|
17
|
+
var projectSlug = getActiveProjectForClient(clientId);
|
|
18
|
+
var active = projectSlug ? getActiveBrainstorm(projectSlug) : getAnyActiveBrainstorm();
|
|
19
|
+
if (active) {
|
|
20
|
+
sendTo(clientId, {
|
|
21
|
+
type: "brainstorm:status",
|
|
22
|
+
active: true,
|
|
23
|
+
html: active.html,
|
|
24
|
+
filename: active.filename,
|
|
25
|
+
sessionDir: active.sessionDir,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
sendTo(clientId, { type: "brainstorm:status", active: false });
|
|
30
|
+
}
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
});
|