@lampdotdev/lamp 0.36.2-nightly.28 → 0.36.2-nightly.30
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/cli/ai.mjs +10 -2
- package/cli/calls.mjs +3 -3
- package/cli/lamps/lamp-for-games.js +9 -3
- package/cli/lamps/lamp.js +9 -3
- package/cli/lamps/mac-starter/.lamp/meta.json +1 -1
- package/cli/lamps/mac-starter/Sources/LampSupport.swift +4 -3
- package/cli/lamps/web-starter/lamp.js +9 -3
- package/package.json +1 -1
- package/site/lib/shared-wishes.js +65 -3
package/cli/ai.mjs
CHANGED
|
@@ -212,6 +212,14 @@ export function models() {
|
|
|
212
212
|
return { models: listedHere, default: defaultId(which) };
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
/**
|
|
216
|
+
* How long a page's request may take: a real answer is never cut off however long the model takes, and the guard only
|
|
217
|
+
* ends a call that has stopped answering (`MAX_TURN_SECONDS` for a step of `act`, the same here). A page that wants a
|
|
218
|
+
* quicker answer asks for one (`seconds`), never a longer one.
|
|
219
|
+
*/
|
|
220
|
+
const GUARD_SECONDS = 600;
|
|
221
|
+
const secondsFor = (body) => (typeof body?.seconds === "number" && body.seconds > 0 ? Math.min(body.seconds, GUARD_SECONDS) : GUARD_SECONDS);
|
|
222
|
+
|
|
215
223
|
/** `/ai`: a text answer, or JSON when asked for it. */
|
|
216
224
|
export async function ask(body) {
|
|
217
225
|
const prompt = typeof body?.prompt === "string" ? body.prompt.trim() : "";
|
|
@@ -219,7 +227,7 @@ export async function ask(body) {
|
|
|
219
227
|
let system = typeof body.system === "string" ? body.system : "";
|
|
220
228
|
if (body.json) system += (system ? "\n\n" : "") + "Answer with JSON only, nothing around it.";
|
|
221
229
|
const which = engine();
|
|
222
|
-
try { return [200, { text: await complete({ system, prompt, images: body.images, which, model: modelFor(which, body.model) }) }]; }
|
|
230
|
+
try { return [200, { text: await complete({ system, prompt, images: body.images, which, model: modelFor(which, body.model), seconds: secondsFor(body) }) }]; }
|
|
223
231
|
catch (error) { return [502, { error: error.message }]; }
|
|
224
232
|
}
|
|
225
233
|
|
|
@@ -233,7 +241,7 @@ export async function decide(body) {
|
|
|
233
241
|
}
|
|
234
242
|
const { system, prompt, schema } = decidePrompt(body.state, questions);
|
|
235
243
|
let text;
|
|
236
|
-
try { text = await complete({ system, prompt, schema }); } catch (error) { return [502, { error: error.message }]; }
|
|
244
|
+
try { text = await complete({ system, prompt, schema, seconds: secondsFor(body) }); } catch (error) { return [502, { error: error.message }]; }
|
|
237
245
|
const read = readDecision(text, questions);
|
|
238
246
|
return read.answers ? [200, { answers: read.answers }] : [502, { error: read.problem }];
|
|
239
247
|
}
|
package/cli/calls.mjs
CHANGED
|
@@ -373,7 +373,7 @@ export const CALLS = {
|
|
|
373
373
|
*/
|
|
374
374
|
async openRouterText(given, { stopped } = {}) {
|
|
375
375
|
return openRouter(() => openRouterText({ key: text(given, "key"), prompt: text(given, "prompt"), images: given.images, system: given.system ?? "",
|
|
376
|
-
model: text(given, "model"), json: given.json === true, schema: given.schema ?? undefined, seconds: seconds(given.seconds,
|
|
376
|
+
model: text(given, "model"), json: given.json === true, schema: given.schema ?? undefined, seconds: seconds(given.seconds, 600, 600), signal: stopped }));
|
|
377
377
|
},
|
|
378
378
|
|
|
379
379
|
/** One chat completion from OpenRouter, its request as `body` (`actAsk`'s): `{message, cost}`, or `{trouble}`. */
|
|
@@ -384,7 +384,7 @@ export const CALLS = {
|
|
|
384
384
|
/** Decisions from OpenRouter's decision model (`AppAI.decide`): `key`, `model`, `state`, `questions`. `{answers, cost}` or `{trouble}`. */
|
|
385
385
|
async openRouterDecide(given, { stopped } = {}) {
|
|
386
386
|
return openRouter(() => openRouterDecide({ key: text(given, "key"), model: text(given, "model"), state: given.state, questions: given.questions ?? {},
|
|
387
|
-
seconds: seconds(given.seconds,
|
|
387
|
+
seconds: seconds(given.seconds, 600, 600), signal: stopped }));
|
|
388
388
|
},
|
|
389
389
|
|
|
390
390
|
/** A picture from OpenRouter (`AppAI.image`): `key`, `model`, `prompt`, `references`, `aspectRatio`. `{image, cost}` or `{trouble}`. */
|
|
@@ -403,7 +403,7 @@ export const CALLS = {
|
|
|
403
403
|
return { text: await complete({ which: text(given, "engine"), tool: typeof given.tool === "string" && given.tool ? given.tool : undefined,
|
|
404
404
|
env: given.env && typeof given.env === "object" ? given.env : {}, system: typeof given.system === "string" ? given.system : "",
|
|
405
405
|
prompt: text(given, "prompt"), schema: given.schema ?? undefined, model: typeof given.model === "string" ? given.model : "",
|
|
406
|
-
images: Array.isArray(given.images) ? given.images : [], seconds: seconds(given.seconds,
|
|
406
|
+
images: Array.isArray(given.images) ? given.images : [], seconds: seconds(given.seconds, 600, 600), signal: stopped }) };
|
|
407
407
|
} catch (error) {
|
|
408
408
|
if (error instanceof EngineTrouble) return { trouble: { kind: error.kind, message: error.message } };
|
|
409
409
|
throw error;
|
|
@@ -301,6 +301,10 @@
|
|
|
301
301
|
});
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
+
// How long the page waits for an answer: as long as Lamp's guard on the call (10 minutes), or the page's own limit,
|
|
305
|
+
// with a little more for the answer to come back.
|
|
306
|
+
function AI_WAIT(seconds) { return (typeof seconds === 'number' && seconds > 0 ? Math.min(seconds, 600) : 600) * 1000 + 15000; }
|
|
307
|
+
|
|
304
308
|
function post(path, body, timeout, deadline) {
|
|
305
309
|
timeout = timeout || 240000;
|
|
306
310
|
var done;
|
|
@@ -485,7 +489,9 @@
|
|
|
485
489
|
return encode(o.images).then(function (images) {
|
|
486
490
|
var body = { prompt: text, system: rules.filter(function (r) { return r; }).join('\n\n'), json: !!o.json, images: images };
|
|
487
491
|
if (o.model) body.model = o.model;
|
|
488
|
-
|
|
492
|
+
// No limit of the page's own unless it asks for one: Lamp's guard ends only a call that stopped answering.
|
|
493
|
+
if (typeof o.seconds === 'number' && o.seconds > 0) body.seconds = o.seconds;
|
|
494
|
+
return post('/ai', body, AI_WAIT(body.seconds));
|
|
489
495
|
}).then(function (reply) {
|
|
490
496
|
if (typeof reply.text !== 'string') throw new Failure('The AI request failed.');
|
|
491
497
|
if (!o.json) return reply.text;
|
|
@@ -998,7 +1004,7 @@
|
|
|
998
1004
|
},
|
|
999
1005
|
/** Asks several questions about the same thing in one request. Answers come back under the same keys. */
|
|
1000
1006
|
decide: function (about, questions) {
|
|
1001
|
-
return post('/ai/decide', { state: decisionState(about), questions: questions }).then(function (reply) {
|
|
1007
|
+
return post('/ai/decide', { state: decisionState(about), questions: questions }, AI_WAIT()).then(function (reply) {
|
|
1002
1008
|
var answers = {};
|
|
1003
1009
|
for (var key in (reply.answers || {})) if (reply.answers.hasOwnProperty(key)) answers[key] = decision(reply.answers[key]);
|
|
1004
1010
|
return answers;
|
|
@@ -1036,7 +1042,7 @@
|
|
|
1036
1042
|
finish(i, answers);
|
|
1037
1043
|
return Promise.resolve();
|
|
1038
1044
|
}
|
|
1039
|
-
return post('/ai/decide', { state: decisionState(state), questions: questions }).then(function (reply) {
|
|
1045
|
+
return post('/ai/decide', { state: decisionState(state), questions: questions }, AI_WAIT()).then(function (reply) {
|
|
1040
1046
|
var answers = {};
|
|
1041
1047
|
for (var k in (reply.answers || {})) if (reply.answers.hasOwnProperty(k)) answers[k] = decision(reply.answers[k]);
|
|
1042
1048
|
if (key) saved[key] = { answers: reply.answers, until: now + o.remember * 1000 };
|
package/cli/lamps/lamp.js
CHANGED
|
@@ -301,6 +301,10 @@
|
|
|
301
301
|
});
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
+
// How long the page waits for an answer: as long as Lamp's guard on the call (10 minutes), or the page's own limit,
|
|
305
|
+
// with a little more for the answer to come back.
|
|
306
|
+
function AI_WAIT(seconds) { return (typeof seconds === 'number' && seconds > 0 ? Math.min(seconds, 600) : 600) * 1000 + 15000; }
|
|
307
|
+
|
|
304
308
|
function post(path, body, timeout, deadline) {
|
|
305
309
|
timeout = timeout || 240000;
|
|
306
310
|
var done;
|
|
@@ -485,7 +489,9 @@
|
|
|
485
489
|
return encode(o.images).then(function (images) {
|
|
486
490
|
var body = { prompt: text, system: rules.filter(function (r) { return r; }).join('\n\n'), json: !!o.json, images: images };
|
|
487
491
|
if (o.model) body.model = o.model;
|
|
488
|
-
|
|
492
|
+
// No limit of the page's own unless it asks for one: Lamp's guard ends only a call that stopped answering.
|
|
493
|
+
if (typeof o.seconds === 'number' && o.seconds > 0) body.seconds = o.seconds;
|
|
494
|
+
return post('/ai', body, AI_WAIT(body.seconds));
|
|
489
495
|
}).then(function (reply) {
|
|
490
496
|
if (typeof reply.text !== 'string') throw new Failure('The AI request failed.');
|
|
491
497
|
if (!o.json) return reply.text;
|
|
@@ -998,7 +1004,7 @@
|
|
|
998
1004
|
},
|
|
999
1005
|
/** Asks several questions about the same thing in one request. Answers come back under the same keys. */
|
|
1000
1006
|
decide: function (about, questions) {
|
|
1001
|
-
return post('/ai/decide', { state: decisionState(about), questions: questions }).then(function (reply) {
|
|
1007
|
+
return post('/ai/decide', { state: decisionState(about), questions: questions }, AI_WAIT()).then(function (reply) {
|
|
1002
1008
|
var answers = {};
|
|
1003
1009
|
for (var key in (reply.answers || {})) if (reply.answers.hasOwnProperty(key)) answers[key] = decision(reply.answers[key]);
|
|
1004
1010
|
return answers;
|
|
@@ -1036,7 +1042,7 @@
|
|
|
1036
1042
|
finish(i, answers);
|
|
1037
1043
|
return Promise.resolve();
|
|
1038
1044
|
}
|
|
1039
|
-
return post('/ai/decide', { state: decisionState(state), questions: questions }).then(function (reply) {
|
|
1045
|
+
return post('/ai/decide', { state: decisionState(state), questions: questions }, AI_WAIT()).then(function (reply) {
|
|
1040
1046
|
var answers = {};
|
|
1041
1047
|
for (var k in (reply.answers || {})) if (reply.answers.hasOwnProperty(k)) answers[k] = decision(reply.answers[k]);
|
|
1042
1048
|
if (key) saved[key] = { answers: reply.answers, until: now + o.remember * 1000 };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"Zqx App Name","
|
|
1
|
+
{"platform":"mac","name":"Zqx App Name","exe":"ZqxAppName","instance":"45F58BDE-3A30-4885-9300-2DCAC8AB84BA"}
|
|
@@ -11299,7 +11299,7 @@
|
|
|
11299
11299
|
let reply = try await post("/ai/decide", [
|
|
11300
11300
|
"state": try decisionState(state),
|
|
11301
11301
|
"questions": questions.mapValues(\.json),
|
|
11302
|
-
])
|
|
11302
|
+
], timeout: 615)
|
|
11303
11303
|
let answers = reply["answers"] as? [String: [String: Any]] ?? [:]
|
|
11304
11304
|
return answers.mapValues(LampDecision.init)
|
|
11305
11305
|
}
|
|
@@ -11427,7 +11427,7 @@
|
|
|
11427
11427
|
group.addTask {
|
|
11428
11428
|
try Task.checkCancellation()
|
|
11429
11429
|
do {
|
|
11430
|
-
let reply = try await post("/ai/decide", body: request.body)
|
|
11430
|
+
let reply = try await post("/ai/decide", body: request.body, timeout: 615)
|
|
11431
11431
|
guard let answers = reply["answers"] as? [String: Any], !answers.isEmpty else {
|
|
11432
11432
|
return Outcome(index: request.index, error: "The decision model didn't answer.")
|
|
11433
11433
|
}
|
|
@@ -11600,7 +11600,8 @@
|
|
|
11600
11600
|
"images": images.compactMap(encode),
|
|
11601
11601
|
]
|
|
11602
11602
|
if let model { body["model"] = model }
|
|
11603
|
-
|
|
11603
|
+
// As long as Lamp's guard on the call: a real answer is never cut off, however long the model takes.
|
|
11604
|
+
let reply = try await post("/ai", body, timeout: 615)
|
|
11604
11605
|
guard let text = reply["text"] as? String else { throw Failure(message: "The AI request failed.") }
|
|
11605
11606
|
return text
|
|
11606
11607
|
}
|
|
@@ -301,6 +301,10 @@
|
|
|
301
301
|
});
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
+
// How long the page waits for an answer: as long as Lamp's guard on the call (10 minutes), or the page's own limit,
|
|
305
|
+
// with a little more for the answer to come back.
|
|
306
|
+
function AI_WAIT(seconds) { return (typeof seconds === 'number' && seconds > 0 ? Math.min(seconds, 600) : 600) * 1000 + 15000; }
|
|
307
|
+
|
|
304
308
|
function post(path, body, timeout, deadline) {
|
|
305
309
|
timeout = timeout || 240000;
|
|
306
310
|
var done;
|
|
@@ -485,7 +489,9 @@
|
|
|
485
489
|
return encode(o.images).then(function (images) {
|
|
486
490
|
var body = { prompt: text, system: rules.filter(function (r) { return r; }).join('\n\n'), json: !!o.json, images: images };
|
|
487
491
|
if (o.model) body.model = o.model;
|
|
488
|
-
|
|
492
|
+
// No limit of the page's own unless it asks for one: Lamp's guard ends only a call that stopped answering.
|
|
493
|
+
if (typeof o.seconds === 'number' && o.seconds > 0) body.seconds = o.seconds;
|
|
494
|
+
return post('/ai', body, AI_WAIT(body.seconds));
|
|
489
495
|
}).then(function (reply) {
|
|
490
496
|
if (typeof reply.text !== 'string') throw new Failure('The AI request failed.');
|
|
491
497
|
if (!o.json) return reply.text;
|
|
@@ -998,7 +1004,7 @@
|
|
|
998
1004
|
},
|
|
999
1005
|
/** Asks several questions about the same thing in one request. Answers come back under the same keys. */
|
|
1000
1006
|
decide: function (about, questions) {
|
|
1001
|
-
return post('/ai/decide', { state: decisionState(about), questions: questions }).then(function (reply) {
|
|
1007
|
+
return post('/ai/decide', { state: decisionState(about), questions: questions }, AI_WAIT()).then(function (reply) {
|
|
1002
1008
|
var answers = {};
|
|
1003
1009
|
for (var key in (reply.answers || {})) if (reply.answers.hasOwnProperty(key)) answers[key] = decision(reply.answers[key]);
|
|
1004
1010
|
return answers;
|
|
@@ -1036,7 +1042,7 @@
|
|
|
1036
1042
|
finish(i, answers);
|
|
1037
1043
|
return Promise.resolve();
|
|
1038
1044
|
}
|
|
1039
|
-
return post('/ai/decide', { state: decisionState(state), questions: questions }).then(function (reply) {
|
|
1045
|
+
return post('/ai/decide', { state: decisionState(state), questions: questions }, AI_WAIT()).then(function (reply) {
|
|
1040
1046
|
var answers = {};
|
|
1041
1047
|
for (var k in (reply.answers || {})) if (reply.answers.hasOwnProperty(k)) answers[k] = decision(reply.answers[k]);
|
|
1042
1048
|
if (key) saved[key] = { answers: reply.answers, until: now + o.remember * 1000 };
|
package/package.json
CHANGED
|
@@ -299,6 +299,7 @@ export function cloudStore(env, fetcher = fetch) {
|
|
|
299
299
|
const accountByKeyKey = (hash) => `lamp:shared-wish:{${scope}}:account-key:${hash}`;
|
|
300
300
|
const accountWaitingKey = (token) => `lamp:shared-wish:{${scope}}:account-waiting:${token}`;
|
|
301
301
|
const appKey = (name, app) => `lamp:shared-wish:{${scope}}:app:${name}:${app}`;
|
|
302
|
+
const copiesKey = (id) => `lamp:shared-wish:{${scope}}:copies:${id}`;
|
|
302
303
|
const command = async (args) => {
|
|
303
304
|
const response = await fetcher(db.url, {
|
|
304
305
|
method: "POST", redirect: "error", signal: AbortSignal.timeout(3000),
|
|
@@ -330,7 +331,19 @@ export function cloudStore(env, fetcher = fetch) {
|
|
|
330
331
|
return (await command(["EVAL", "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('SET', KEYS[1], ARGV[2]) end return false",
|
|
331
332
|
"1", key(after.id), JSON.stringify(before), JSON.stringify(after)])) === "OK";
|
|
332
333
|
},
|
|
333
|
-
async remove(id) { await command(["DEL", key(id), ideasKey(id), makerKey(id)]); },
|
|
334
|
+
async remove(id) { await command(["DEL", key(id), ideasKey(id), makerKey(id), copiesKey(id)]); },
|
|
335
|
+
/** The links shared from copies of a link (each record's `from`), by id: a copy taken down since is let go when listed. */
|
|
336
|
+
/** Kept by when each was last shared, so the newest come first however many there are. */
|
|
337
|
+
async addCopy(id, copy, at = Date.now()) { await command(["ZADD", copiesKey(id), String(at), copy]); },
|
|
338
|
+
async copies(id, count = 200) { const ids = await command(["ZREVRANGE", copiesKey(id), "0", String(count - 1)]); return Array.isArray(ids) ? ids : []; },
|
|
339
|
+
async copyCount(id) { const n = await command(["ZCARD", copiesKey(id)]); return Number(n) || 0; },
|
|
340
|
+
async removeCopy(id, copy) { await command(["ZREM", copiesKey(id), copy]); },
|
|
341
|
+
/** Several records at once, in one round trip, null for one that's gone. */
|
|
342
|
+
async getMany(ids) {
|
|
343
|
+
if (!ids.length) return [];
|
|
344
|
+
const texts = await command(["MGET", ...ids.map(key)]);
|
|
345
|
+
return (Array.isArray(texts) ? texts : []).map((text) => (typeof text === "string" ? JSON.parse(text) : null));
|
|
346
|
+
},
|
|
334
347
|
/** A maker's account by its username: `{ name, email, created }`, or null. */
|
|
335
348
|
async account(name) { const text = await command(["GET", accountKey(name)]); return typeof text === "string" ? JSON.parse(text) : null; },
|
|
336
349
|
async accountForEmail(email) { const name = await command(["GET", accountEmailKey(email)]); return typeof name === "string" ? name : null; },
|
|
@@ -583,7 +596,7 @@ function remember(url, bytes) {
|
|
|
583
596
|
|
|
584
597
|
/** Kept in memory, for `npm run dev` and the tests: pictures as data: URLs, so nothing else needs serving. */
|
|
585
598
|
export function memoryStore() {
|
|
586
|
-
const records = new Map(), pages = new Map(), ideas = new Map(), mailboxes = new Map(), makers = new Map();
|
|
599
|
+
const records = new Map(), pages = new Map(), ideas = new Map(), mailboxes = new Map(), makers = new Map(), copies = new Map();
|
|
587
600
|
const accounts = new Map(), emails = new Map(), keys = new Map(), waiting = new Map(), apps = new Map();
|
|
588
601
|
return {
|
|
589
602
|
limiter: null,
|
|
@@ -611,7 +624,12 @@ export function memoryStore() {
|
|
|
611
624
|
records.set(after.id, structuredClone(after));
|
|
612
625
|
return true;
|
|
613
626
|
},
|
|
614
|
-
async remove(id) { records.delete(id); ideas.delete(id); makers.delete(id); },
|
|
627
|
+
async remove(id) { records.delete(id); ideas.delete(id); makers.delete(id); copies.delete(id); },
|
|
628
|
+
async addCopy(id, copy, at = Date.now()) { if (!copies.has(id)) copies.set(id, new Map()); copies.get(id).set(copy, at); },
|
|
629
|
+
async copies(id, count = 200) { return [...(copies.get(id) ?? new Map())].sort((a, b) => b[1] - a[1]).slice(0, count).map(([copy]) => copy); },
|
|
630
|
+
async copyCount(id) { return copies.get(id)?.size ?? 0; },
|
|
631
|
+
async removeCopy(id, copy) { copies.get(id)?.delete(copy); },
|
|
632
|
+
async getMany(ids) { return ids.map((id) => (records.has(id) ? structuredClone(records.get(id)) : null)); },
|
|
615
633
|
async maker(id) { return makers.has(id) ? structuredClone(makers.get(id)) : null; },
|
|
616
634
|
async setMaker(id, value) { if (value) makers.set(id, structuredClone(value)); else makers.delete(id); },
|
|
617
635
|
async makerStep(id, key, step) {
|
|
@@ -705,6 +723,42 @@ const bearer = (request) => /^Bearer (\S+)$/.exec(request.headers.get("authoriza
|
|
|
705
723
|
* keeping the link; DELETE ?id= with the secret takes it down. Lamp is the only caller: a browser on another site
|
|
706
724
|
* is refused, and nothing a request holds is logged.
|
|
707
725
|
*/
|
|
726
|
+
/**
|
|
727
|
+
* A copy of a link was shared (`from`): kept among the link's copies at every share, so one missing from the index (shared
|
|
728
|
+
* before it, or a write that failed) comes back, and moved up as the newest; its maker told by email at the address that
|
|
729
|
+
* confirmed their account (or the link's own maker address) only the first time (`first`). Never fails the share.
|
|
730
|
+
*/
|
|
731
|
+
async function copied(kept, from, copy, origin, { env, fetcher, first }) {
|
|
732
|
+
try { await kept.addCopy(from, copy.id); } catch {}
|
|
733
|
+
if (!first) return;
|
|
734
|
+
try {
|
|
735
|
+
const source = await kept.get(from);
|
|
736
|
+
if (!source) return;
|
|
737
|
+
const account = source.owner ? await kept.account(source.owner) : null;
|
|
738
|
+
const maker = account ? null : await kept.maker(from);
|
|
739
|
+
const to = account?.email ?? (maker?.confirmed ? maker.email : null);
|
|
740
|
+
if (!to) return;
|
|
741
|
+
const who = copy.owner || copy.by || "Someone";
|
|
742
|
+
const theirs = linkOf(origin, copy), yours = linkOf(origin, source);
|
|
743
|
+
await sendMail(env, fetcher, { to: [to], subject: `${who} made their own ${source.name}`,
|
|
744
|
+
text: `${who} made their own copy of ${source.name} and shared it:\n\n${theirs}\n\nEvery copy of yours is listed on its page: ${yours}\n` }, `copy-${from}-${copy.id}`);
|
|
745
|
+
} catch {}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/** The copies of a link as its page lists them, newest first; one taken down since is let go. */
|
|
749
|
+
export async function copiesOf(kept, id, origin) {
|
|
750
|
+
const found = [];
|
|
751
|
+
// The newest 200, by when each was last shared, read in one round trip.
|
|
752
|
+
const ids = await kept.copies(id, 200);
|
|
753
|
+
const records = await kept.getMany(ids);
|
|
754
|
+
for (const [i, copy] of ids.entries()) {
|
|
755
|
+
const record = records[i];
|
|
756
|
+
if (!record || record.from !== id) { try { await kept.removeCopy(id, copy); } catch {} continue; }
|
|
757
|
+
found.push({ url: linkOf(origin, record), name: record.name, by: record.owner || record.by || "", updated: record.updated ?? record.created ?? null });
|
|
758
|
+
}
|
|
759
|
+
return found.sort((a, b) => String(b.updated).localeCompare(String(a.updated)));
|
|
760
|
+
}
|
|
761
|
+
|
|
708
762
|
export function createShareHandler({ env = process.env, fetcher = fetch, store, limited = true, now = () => new Date() } = {}) {
|
|
709
763
|
return async (request) => {
|
|
710
764
|
const method = request.method;
|
|
@@ -721,6 +775,10 @@ export function createShareHandler({ env = process.env, fetcher = fetch, store,
|
|
|
721
775
|
if (method === "GET") {
|
|
722
776
|
let kept;
|
|
723
777
|
try { kept = store ?? cloudStore(env, fetcher); } catch { return reply(503, { error: "service_unavailable" }); }
|
|
778
|
+
// The copies people shared of this link, newest first: public, as every link is.
|
|
779
|
+
if (url.searchParams.get("copies") === "1" && isID(id)) {
|
|
780
|
+
try { return reply(200, { copies: await copiesOf(kept, id, url.origin) }); } catch { return reply(503, { error: "service_unavailable" }); }
|
|
781
|
+
}
|
|
724
782
|
try {
|
|
725
783
|
const found = byName ? await kept.appID(user, app) : id;
|
|
726
784
|
const record = found ? await kept.get(found) : null;
|
|
@@ -786,6 +844,7 @@ export function createShareHandler({ env = process.env, fetcher = fetch, store,
|
|
|
786
844
|
if (method === "PUT") {
|
|
787
845
|
const record = { ...earlier, ...words, ...named, updated: time };
|
|
788
846
|
await kept.replace(record);
|
|
847
|
+
if (record.from) await copied(kept, record.from, record, url.origin, { env, fetcher, first: !!from && earlier?.from !== from });
|
|
789
848
|
// Kept: the name is the record's now, whatever the cleanup after it does.
|
|
790
849
|
uploaded = [];
|
|
791
850
|
claimed = null;
|
|
@@ -800,6 +859,7 @@ export function createShareHandler({ env = process.env, fetcher = fetch, store,
|
|
|
800
859
|
}
|
|
801
860
|
uploaded = [];
|
|
802
861
|
claimed = null;
|
|
862
|
+
if (from) await copied(kept, from, record, url.origin, { env, fetcher, first: true });
|
|
803
863
|
return reply(201, { id: wishID, url: linkOf(url.origin, record), secret });
|
|
804
864
|
} catch {
|
|
805
865
|
try { await kept.removeImages(uploaded); } catch {}
|
|
@@ -1305,6 +1365,8 @@ export async function findWish(id, { env = process.env, fetcher = fetch, store,
|
|
|
1305
1365
|
if (source) wish.from = { url: linkOf(origin, source), label: source.owner && source.app ? `${source.owner}/${source.app}` : source.name };
|
|
1306
1366
|
} catch {}
|
|
1307
1367
|
}
|
|
1368
|
+
// How many copies of it people shared, for the page's "copies" (the list itself is asked for when opened).
|
|
1369
|
+
if (wish && origin) { try { wish.copies = await kept.copyCount(id); } catch {} }
|
|
1308
1370
|
return wish;
|
|
1309
1371
|
}
|
|
1310
1372
|
|