@lampdotdev/lamp 0.36.2-nightly.29 → 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.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"Zqx App Name","
|
|
1
|
+
{"platform":"mac","name":"Zqx App Name","exe":"ZqxAppName","instance":"45F58BDE-3A30-4885-9300-2DCAC8AB84BA"}
|
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
|
|