@justai/cuts 0.38.0 → 0.39.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/package.json +1 -1
- package/src/FilePicker.astro +65 -24
package/package.json
CHANGED
package/src/FilePicker.astro
CHANGED
|
@@ -116,8 +116,21 @@ const { id, title = "Choose a file", src = "/files" } = Astro.props;
|
|
|
116
116
|
import atrium from "@justai/atrium";
|
|
117
117
|
import { PRESENTATION_PARAM, PRESENTATION_EMBEDDED } from "@justai/core";
|
|
118
118
|
|
|
119
|
+
// queries: file id or path · which one should a persona remember · does renaming break a
|
|
120
|
+
// reference · the durable handle for a file
|
|
121
|
+
//
|
|
122
|
+
// A **path** is where a file is standing right now, and it is how a PERSON says where something
|
|
123
|
+
// is. Rename it and every path to it is a dangling string that still looks perfectly valid. An
|
|
124
|
+
// **id** is the file: the kernel has stamped a `crypto.randomUUID()` on every node since the
|
|
125
|
+
// first write, and `mv` never touches it.
|
|
126
|
+
//
|
|
127
|
+
// So a caller reacting to something a person just did may use a path. A caller REMEMBERING a
|
|
128
|
+
// file across time — a project pointing at its logo, a document being saved back — must use the
|
|
129
|
+
// id, and both are handed over here so that choice is available rather than argued for.
|
|
119
130
|
interface Picked {
|
|
120
131
|
path: string;
|
|
132
|
+
/** WHAT IT IS, not where it is standing. Survives a rename and a move — remember this one. */
|
|
133
|
+
id: string;
|
|
121
134
|
name: string;
|
|
122
135
|
mime: string;
|
|
123
136
|
bytes: ArrayBuffer;
|
|
@@ -131,8 +144,19 @@ const { id, title = "Choose a file", src = "/files" } = Astro.props;
|
|
|
131
144
|
* What a persona hands over to be kept. `name` is a PROPOSAL — the person may rewrite it and
|
|
132
145
|
* will choose the folder — so give it the best default you have, extension included.
|
|
133
146
|
*/
|
|
134
|
-
interface SaveRequest {
|
|
135
|
-
|
|
147
|
+
interface SaveRequest {
|
|
148
|
+
/** A PROPOSAL. The person may rewrite it — the extension is shown beside the field, not in it. */
|
|
149
|
+
name?: string;
|
|
150
|
+
/** The persona's format. Kept out of the name box, the way a desktop save dialog keeps it out. */
|
|
151
|
+
ext?: string;
|
|
152
|
+
/** Save BACK to a file already saved once. NO QUESTION IS ASKED: it was answered the first time. */
|
|
153
|
+
id?: string;
|
|
154
|
+
bytes: ArrayBuffer | Uint8Array;
|
|
155
|
+
mime?: string;
|
|
156
|
+
title?: string;
|
|
157
|
+
timeout?: number;
|
|
158
|
+
}
|
|
159
|
+
interface Saved { path: string; id: string; name: string }
|
|
136
160
|
|
|
137
161
|
for (const root of document.querySelectorAll<HTMLElement>("[data-picker]")) {
|
|
138
162
|
const id = root.dataset.picker!;
|
|
@@ -180,20 +204,26 @@ const { id, title = "Choose a file", src = "/files" } = Astro.props;
|
|
|
180
204
|
// for a file and for handing one over — the lazy frame, the presentation declaration, the
|
|
181
205
|
// heading, the two ways a sheet can end. Extracted so the second question inherits the first
|
|
182
206
|
// one's scars instead of being written again and re-earning them.
|
|
207
|
+
// Set the frame's src on first use. THREE callers need it now, and the presentation declaration
|
|
208
|
+
// must be identical in all three — a fourth that copied it slightly wrong is how a page ends up
|
|
209
|
+
// announcing itself as standalone on the one path that forgot to say otherwise.
|
|
210
|
+
function ensureFrame() {
|
|
211
|
+
if (frame.getAttribute("src")) return;
|
|
212
|
+
// DECLARE THE PRESENTATION. A page opened by a person and a page opened by another persona
|
|
213
|
+
// are two different situations, and the same controls do not belong in both — standalone
|
|
214
|
+
// there is nothing to cancel. The frame is the slower truth (a handshake takes a beat), so
|
|
215
|
+
// the declaration rides in the URL and is available in the first paint. It is presentation
|
|
216
|
+
// only: nothing that must not be forged is decided by it.
|
|
217
|
+
const src = root.dataset.src!;
|
|
218
|
+
frame.setAttribute("src", src + (src.includes("?") ? "&" : "?") + PRESENTATION_PARAM + "=" + PRESENTATION_EMBEDDED);
|
|
219
|
+
}
|
|
220
|
+
|
|
183
221
|
async function ask<T>(
|
|
184
222
|
req: { title?: string },
|
|
185
223
|
invoke: (bus: any) => Promise<T>,
|
|
186
224
|
): Promise<T | null> {
|
|
187
225
|
const bus = await router();
|
|
188
|
-
|
|
189
|
-
// DECLARE THE PRESENTATION. A page opened by a person and a page opened by another persona
|
|
190
|
-
// are two different situations, and the same controls do not belong in both — standalone
|
|
191
|
-
// there is nothing to cancel. The frame is the slower truth (a handshake takes a beat), so
|
|
192
|
-
// the declaration rides in the URL and is available in the first paint. It is presentation
|
|
193
|
-
// only: nothing that must not be forged is decided by it.
|
|
194
|
-
const src = root.dataset.src!;
|
|
195
|
-
frame.setAttribute("src", src + (src.includes("?") ? "&" : "?") + PRESENTATION_PARAM + "=" + PRESENTATION_EMBEDDED);
|
|
196
|
-
}
|
|
226
|
+
ensureFrame();
|
|
197
227
|
// ONE HEADING, and it is the sheet's. A per-call title retitles the sheet rather than being
|
|
198
228
|
// forwarded into the frame: the kernel renders a heading of its own for callers that have no
|
|
199
229
|
// sheet around them, and sending it one here printed "Upload logo" twice, one under the other.
|
|
@@ -275,13 +305,24 @@ const { id, title = "Choose a file", src = "/files" } = Astro.props;
|
|
|
275
305
|
const b: any = req.bytes;
|
|
276
306
|
const bytes: ArrayBuffer =
|
|
277
307
|
b instanceof ArrayBuffer ? b : b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength);
|
|
308
|
+
const args = { name: req.name, ext: req.ext, id: req.id, bytes, mime: req.mime };
|
|
309
|
+
// SAVING BACK OPENS NO SHEET, and that is the point rather than an optimisation. The person
|
|
310
|
+
// named this file the first time; asking again on every save is not care, it is a persona
|
|
311
|
+
// that cannot remember what it is working on. So this path never touches `ask`.
|
|
312
|
+
//
|
|
313
|
+
// BY ID AND NOT BY PATH: between opening a document and saving it, the person may have
|
|
314
|
+
// renamed or moved it in the very list they are looking at. A path captured at open time
|
|
315
|
+
// would then either miss — a second copy appearing under the old name — or land on whatever
|
|
316
|
+
// has since taken that name, which is worse and completely silent.
|
|
317
|
+
if (req.id) {
|
|
318
|
+
const bus = await router();
|
|
319
|
+
ensureFrame();
|
|
320
|
+
return arrived!
|
|
321
|
+
.then(() => bus.proxy({ service: "files", method: "save", args, options: { timeout: req.timeout ?? 60000 } }))
|
|
322
|
+
.catch(() => null) as Promise<Saved | null>;
|
|
323
|
+
}
|
|
278
324
|
return ask(req, (bus) =>
|
|
279
|
-
bus.proxy({
|
|
280
|
-
service: "files",
|
|
281
|
-
method: "save",
|
|
282
|
-
args: { name: req.name, bytes, mime: req.mime },
|
|
283
|
-
options: { timeout: req.timeout ?? 300000 },
|
|
284
|
-
}));
|
|
325
|
+
bus.proxy({ service: "files", method: "save", args, options: { timeout: req.timeout ?? 300000 } }));
|
|
285
326
|
}
|
|
286
327
|
|
|
287
328
|
// queries: read a file the persona already has a path to · FilePicker read · restore an asset a
|
|
@@ -302,16 +343,16 @@ const { id, title = "Choose a file", src = "/files" } = Astro.props;
|
|
|
302
343
|
// It opens no sheet, because there is no question. The policy still answers "who is asking" —
|
|
303
344
|
// same-origin only, exactly as every other capability — so this widens no door: a caller that
|
|
304
345
|
// could not read here a moment ago still cannot.
|
|
305
|
-
async function read(
|
|
346
|
+
async function read(ref: string | { path?: string; id?: string }): Promise<{ bytes: ArrayBuffer; blob(): Blob; dataUrl(): Promise<string> } | null> {
|
|
347
|
+
// A bare string is a PATH, because that is what a string reads as. An id must be said out
|
|
348
|
+
// loud — `read({ id })` — so a caller holding one kind can never pass it as the other.
|
|
349
|
+
const where = typeof ref === "string" ? { path: ref } : ref;
|
|
306
350
|
const bus = await router();
|
|
307
|
-
|
|
308
|
-
const src = root.dataset.src!;
|
|
309
|
-
frame.setAttribute("src", src + (src.includes("?") ? "&" : "?") + PRESENTATION_PARAM + "=" + PRESENTATION_EMBEDDED);
|
|
310
|
-
}
|
|
351
|
+
ensureFrame();
|
|
311
352
|
const [bytes, stat] = await arrived!
|
|
312
353
|
.then(() => Promise.all([
|
|
313
|
-
bus.proxy({ service: "files", method: "read", args:
|
|
314
|
-
bus.proxy({ service: "files", method: "stat", args:
|
|
354
|
+
bus.proxy({ service: "files", method: "read", args: where, options: { timeout: 30000 } }),
|
|
355
|
+
bus.proxy({ service: "files", method: "stat", args: where, options: { timeout: 30000 } }),
|
|
315
356
|
]))
|
|
316
357
|
.catch(() => [null, null]);
|
|
317
358
|
// A file the person deleted since is simply gone. `null` rather than a throw: a caller
|