@graffiti-garden/implementation-decentralized 0.1.3 → 0.1.4
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/dist/1-services/4-inboxes/cache.d.ts +34 -0
- package/dist/1-services/4-inboxes/cache.d.ts.map +1 -0
- package/dist/1-services/4-inboxes/messages.d.ts +70 -0
- package/dist/1-services/4-inboxes/messages.d.ts.map +1 -0
- package/dist/1-services/4-inboxes.d.ts +28 -67
- package/dist/1-services/4-inboxes.d.ts.map +1 -1
- package/dist/3-protocol/4-graffiti.d.ts +1 -0
- package/dist/3-protocol/4-graffiti.d.ts.map +1 -1
- package/dist/browser/index.js +7 -7
- package/dist/browser/index.js.map +4 -4
- package/dist/cjs/1-services/4-inboxes/cache.js +186 -0
- package/dist/cjs/1-services/4-inboxes/cache.js.map +7 -0
- package/dist/cjs/1-services/4-inboxes/messages.js +69 -0
- package/dist/cjs/1-services/4-inboxes/messages.js.map +7 -0
- package/dist/cjs/1-services/4-inboxes-tests.js +339 -9
- package/dist/cjs/1-services/4-inboxes-tests.js.map +3 -3
- package/dist/cjs/1-services/4-inboxes.js +210 -399
- package/dist/cjs/1-services/4-inboxes.js.map +3 -3
- package/dist/cjs/3-protocol/4-graffiti.js +19 -10
- package/dist/cjs/3-protocol/4-graffiti.js.map +2 -2
- package/dist/cjs/index.spec.js +1 -0
- package/dist/cjs/index.spec.js.map +2 -2
- package/dist/esm/1-services/4-inboxes/cache.js +156 -0
- package/dist/esm/1-services/4-inboxes/cache.js.map +7 -0
- package/dist/esm/1-services/4-inboxes/messages.js +60 -0
- package/dist/esm/1-services/4-inboxes/messages.js.map +7 -0
- package/dist/esm/1-services/4-inboxes-tests.js +340 -10
- package/dist/esm/1-services/4-inboxes-tests.js.map +3 -3
- package/dist/esm/1-services/4-inboxes.js +222 -383
- package/dist/esm/1-services/4-inboxes.js.map +3 -3
- package/dist/esm/3-protocol/4-graffiti.js +19 -10
- package/dist/esm/3-protocol/4-graffiti.js.map +2 -2
- package/dist/esm/index.spec.js +2 -1
- package/dist/esm/index.spec.js.map +2 -2
- package/package.json +1 -1
- package/src/1-services/4-inboxes/cache.ts +227 -0
- package/src/1-services/4-inboxes/messages.ts +63 -0
- package/src/1-services/4-inboxes-tests.ts +387 -2
- package/src/1-services/4-inboxes.ts +297 -512
- package/src/3-protocol/4-graffiti.ts +26 -10
- package/src/index.spec.ts +3 -1
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
var cache_exports = {};
|
|
30
|
+
__export(cache_exports, {
|
|
31
|
+
canUseIDB: () => canUseIDB,
|
|
32
|
+
createCache: () => createCache,
|
|
33
|
+
getMessageCacheKey: () => getMessageCacheKey,
|
|
34
|
+
getMessageIdsCacheKey: () => getMessageIdsCacheKey,
|
|
35
|
+
replayQuery: () => replayQuery,
|
|
36
|
+
storeQueryPage: () => storeQueryPage
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(cache_exports);
|
|
39
|
+
var import_dag_cbor = require("@ipld/dag-cbor");
|
|
40
|
+
var import_messages = require("./messages");
|
|
41
|
+
async function canUseIDB() {
|
|
42
|
+
try {
|
|
43
|
+
if (!globalThis.indexedDB) return false;
|
|
44
|
+
await new Promise((resolve, reject) => {
|
|
45
|
+
const req = indexedDB.open("__idb_probe__", 1);
|
|
46
|
+
req.onupgradeneeded = () => req.result.createObjectStore("k");
|
|
47
|
+
req.onsuccess = () => {
|
|
48
|
+
req.result.close();
|
|
49
|
+
resolve();
|
|
50
|
+
};
|
|
51
|
+
req.onerror = () => reject(req.error);
|
|
52
|
+
});
|
|
53
|
+
return true;
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function getMessageCacheKey(inboxUrl, messageId) {
|
|
59
|
+
return `${encodeURIComponent(inboxUrl)}:${encodeURIComponent(messageId)}`;
|
|
60
|
+
}
|
|
61
|
+
async function replayQuery(cache, inboxUrl, messageIdsCacheKey, query, numSeen = 0) {
|
|
62
|
+
const messageIds = query.messageIds.slice(numSeen);
|
|
63
|
+
return Promise.all(
|
|
64
|
+
messageIds.map(async (id) => {
|
|
65
|
+
const message = await cache.messages.get(getMessageCacheKey(inboxUrl, id));
|
|
66
|
+
if (!message) {
|
|
67
|
+
try {
|
|
68
|
+
await cache.messageIds.del(messageIdsCacheKey);
|
|
69
|
+
} catch {
|
|
70
|
+
}
|
|
71
|
+
throw new Error("Cache out of sync - perhaps clear browser storage");
|
|
72
|
+
}
|
|
73
|
+
return message;
|
|
74
|
+
})
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
async function storeQueryPage(cache, inboxUrl, messageIdsCacheKey, previous, page) {
|
|
78
|
+
await cache.messages.setMany(
|
|
79
|
+
page.messages.map((message) => [
|
|
80
|
+
getMessageCacheKey(inboxUrl, message[import_messages.LABELED_MESSAGE_ID_KEY]),
|
|
81
|
+
message
|
|
82
|
+
])
|
|
83
|
+
);
|
|
84
|
+
const query = {
|
|
85
|
+
cursor: page.cursor,
|
|
86
|
+
version: previous?.version ?? crypto.randomUUID(),
|
|
87
|
+
messageIds: [
|
|
88
|
+
...previous?.messageIds ?? [],
|
|
89
|
+
...page.messages.map((message) => message[import_messages.LABELED_MESSAGE_ID_KEY])
|
|
90
|
+
],
|
|
91
|
+
generation: crypto.randomUUID(),
|
|
92
|
+
hasMore: page.hasMore,
|
|
93
|
+
waitTil: page.waitTil
|
|
94
|
+
};
|
|
95
|
+
await cache.messageIds.set(messageIdsCacheKey, query);
|
|
96
|
+
return query;
|
|
97
|
+
}
|
|
98
|
+
async function getMessageIdsCacheKey(inboxUrl, type, body) {
|
|
99
|
+
const cacheIdData = (0, import_dag_cbor.encode)({
|
|
100
|
+
inboxUrl,
|
|
101
|
+
type,
|
|
102
|
+
body: body ?? null
|
|
103
|
+
});
|
|
104
|
+
return crypto.subtle.digest("SHA-256", new Uint8Array(cacheIdData)).then(
|
|
105
|
+
(bytes) => Array.from(new Uint8Array(bytes)).map((byte) => byte.toString(16).padStart(2, "0")).join("")
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
async function resetCacheDB() {
|
|
109
|
+
await new Promise((resolve) => {
|
|
110
|
+
const req = indexedDB.deleteDatabase("graffiti-inbox-cache");
|
|
111
|
+
req.onsuccess = () => resolve();
|
|
112
|
+
req.onerror = () => resolve();
|
|
113
|
+
req.onblocked = () => resolve();
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
async function createCache() {
|
|
117
|
+
if (await canUseIDB()) {
|
|
118
|
+
const { openDB } = await import("idb");
|
|
119
|
+
const db = await openDB("graffiti-inbox-cache", 1, {
|
|
120
|
+
upgrade(db2) {
|
|
121
|
+
if (!db2.objectStoreNames.contains("m")) db2.createObjectStore("m");
|
|
122
|
+
if (!db2.objectStoreNames.contains("q")) db2.createObjectStore("q");
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
return {
|
|
126
|
+
messages: {
|
|
127
|
+
get: async (k) => {
|
|
128
|
+
try {
|
|
129
|
+
return db.get("m", k);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.error("Error getting message from cache:", error);
|
|
132
|
+
console.error("resetting cache...");
|
|
133
|
+
await resetCacheDB();
|
|
134
|
+
return void 0;
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
set: async (k, v) => {
|
|
138
|
+
await db.put("m", v, k);
|
|
139
|
+
},
|
|
140
|
+
setMany: async (entries) => {
|
|
141
|
+
if (entries.length === 0) return;
|
|
142
|
+
const tx = db.transaction("m", "readwrite");
|
|
143
|
+
await Promise.all([
|
|
144
|
+
...entries.map(([k, v]) => tx.store.put(v, k)),
|
|
145
|
+
tx.done
|
|
146
|
+
]);
|
|
147
|
+
},
|
|
148
|
+
del: (k) => db.delete("m", k)
|
|
149
|
+
},
|
|
150
|
+
messageIds: {
|
|
151
|
+
get: async (k) => {
|
|
152
|
+
try {
|
|
153
|
+
return await db.get("q", k);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error("Error getting message IDs from cache:", error);
|
|
156
|
+
console.error("resetting cache...");
|
|
157
|
+
await resetCacheDB();
|
|
158
|
+
return void 0;
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
set: async (k, v) => {
|
|
162
|
+
await db.put("q", v, k);
|
|
163
|
+
},
|
|
164
|
+
del: (k) => db.delete("q", k)
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const m = /* @__PURE__ */ new Map();
|
|
169
|
+
const q = /* @__PURE__ */ new Map();
|
|
170
|
+
return {
|
|
171
|
+
messages: {
|
|
172
|
+
get: async (k) => m.get(k),
|
|
173
|
+
set: async (k, v) => void m.set(k, v),
|
|
174
|
+
setMany: async (entries) => {
|
|
175
|
+
for (const [k, v] of entries) m.set(k, v);
|
|
176
|
+
},
|
|
177
|
+
del: async (k) => void m.delete(k)
|
|
178
|
+
},
|
|
179
|
+
messageIds: {
|
|
180
|
+
get: async (k) => q.get(k),
|
|
181
|
+
set: async (k, v) => void q.set(k, v),
|
|
182
|
+
del: async (k) => void q.delete(k)
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/1-services/4-inboxes/cache.ts"],
|
|
4
|
+
"sourcesContent": ["import { encode as dagCborEncode } from \"@ipld/dag-cbor\";\nimport { LABELED_MESSAGE_ID_KEY, type LabeledMessageBase } from \"./messages\";\n\n// One record per inbox query. Messages live in the separate message cache;\n// this record tracks their IDs and where to continue fetching from the server.\nexport type CacheQueryValue = {\n // Server continuation state for the next page.\n cursor: string;\n // Identifies this local query history, not a server version. It stays the\n // same as pages arrive; a reset creates a new one so old caller cursors expire.\n version: string;\n // All message IDs fetched for this query so far, in order. A caller's\n // numSeen cursor indexes into this list to replay only unseen messages.\n messageIds: string[];\n waitTil?: number;\n // Changes on every cached page write, even if the page is empty and neither\n // messageIds nor cursor changes. A tab waiting on the query lock can use this\n // to detect and reuse a page fetched by another tab. Older entries lack it.\n generation?: string;\n // Whether the server has another page; needed when reusing another tab's\n // cached page. Older cache entries lack this and are refreshed on next query.\n hasMore?: boolean;\n};\n\nexport async function canUseIDB(): Promise<boolean> {\n try {\n if (!globalThis.indexedDB) return false;\n\n // Small probe database\n await new Promise<void>((resolve, reject) => {\n const req = indexedDB.open(\"__idb_probe__\", 1);\n req.onupgradeneeded = () => req.result.createObjectStore(\"k\");\n req.onsuccess = () => {\n req.result.close();\n resolve();\n };\n req.onerror = () => reject(req.error);\n });\n\n return true;\n } catch {\n return false;\n }\n}\n\nexport type Cache = {\n messages: {\n get(k: string): Promise<LabeledMessageBase | null | undefined>;\n set(k: string, value: LabeledMessageBase | null): Promise<void>;\n setMany(entries: [string, LabeledMessageBase][]): Promise<void>;\n del(k: string): Promise<void>;\n };\n messageIds: {\n get(k: string): Promise<CacheQueryValue | undefined>;\n set(k: string, value: CacheQueryValue): Promise<void>;\n del(k: string): Promise<void>;\n };\n};\n\nexport function getMessageCacheKey(inboxUrl: string, messageId: string) {\n return `${encodeURIComponent(inboxUrl)}:${encodeURIComponent(messageId)}`;\n}\n\nexport async function replayQuery(\n cache: Cache,\n inboxUrl: string,\n messageIdsCacheKey: string,\n query: CacheQueryValue,\n numSeen = 0,\n): Promise<LabeledMessageBase[]> {\n // Filter out all messageIds before\n // the number already seen\n const messageIds = query.messageIds.slice(numSeen);\n\n // Get all the messages pointed to in the cache\n return Promise.all(\n messageIds.map(async (id) => {\n const message = await cache.messages.get(getMessageCacheKey(inboxUrl, id));\n if (!message) {\n // Something is very wrong with the cache,\n // it refers to message IDs that are not cached\n try {\n await cache.messageIds.del(messageIdsCacheKey);\n } catch {}\n throw new Error(\"Cache out of sync - perhaps clear browser storage\");\n }\n return message;\n }),\n );\n}\n\nexport async function storeQueryPage(\n cache: Cache,\n inboxUrl: string,\n messageIdsCacheKey: string,\n previous: CacheQueryValue | undefined,\n page: {\n messages: LabeledMessageBase[];\n cursor: string;\n hasMore: boolean;\n waitTil?: number;\n },\n): Promise<CacheQueryValue> {\n // First cache the messages with their labels\n await cache.messages.setMany(\n page.messages.map((message) => [\n getMessageCacheKey(inboxUrl, message[LABELED_MESSAGE_ID_KEY]),\n message,\n ]),\n );\n // Then store all the messageids\n const query = {\n cursor: page.cursor,\n version: previous?.version ?? crypto.randomUUID(),\n messageIds: [\n ...(previous?.messageIds ?? []),\n ...page.messages.map((message) => message[LABELED_MESSAGE_ID_KEY]),\n ],\n generation: crypto.randomUUID(),\n hasMore: page.hasMore,\n waitTil: page.waitTil,\n };\n await cache.messageIds.set(messageIdsCacheKey, query);\n return query;\n}\n\nexport async function getMessageIdsCacheKey(\n inboxUrl: string,\n type: \"query\" | \"export\",\n body?: Uint8Array,\n): Promise<string> {\n const cacheIdData = dagCborEncode({\n inboxUrl,\n type,\n body: body ?? null,\n });\n return crypto.subtle\n .digest(\"SHA-256\", new Uint8Array(cacheIdData))\n .then((bytes) =>\n Array.from(new Uint8Array(bytes))\n .map((byte) => byte.toString(16).padStart(2, \"0\"))\n .join(\"\"),\n );\n}\n\nasync function resetCacheDB() {\n await new Promise<void>((resolve) => {\n const req = indexedDB.deleteDatabase(\"graffiti-inbox-cache\");\n req.onsuccess = () => resolve();\n req.onerror = () => resolve(); // best effort\n req.onblocked = () => resolve(); // best effort\n });\n}\n\nexport async function createCache(): Promise<Cache> {\n if (await canUseIDB()) {\n const { openDB } = await import(\"idb\");\n const db = await openDB(\"graffiti-inbox-cache\", 1, {\n upgrade(db) {\n if (!db.objectStoreNames.contains(\"m\")) db.createObjectStore(\"m\");\n if (!db.objectStoreNames.contains(\"q\")) db.createObjectStore(\"q\");\n },\n });\n\n return {\n messages: {\n get: async (k) => {\n try {\n return db.get(\"m\", k);\n } catch (error) {\n console.error(\"Error getting message from cache:\", error);\n console.error(\"resetting cache...\");\n await resetCacheDB();\n return undefined;\n }\n },\n set: async (k, v) => {\n await db.put(\"m\", v, k);\n },\n setMany: async (entries) => {\n if (entries.length === 0) return;\n const tx = db.transaction(\"m\", \"readwrite\");\n await Promise.all([\n ...entries.map(([k, v]) => tx.store.put(v, k)),\n tx.done,\n ]);\n },\n del: (k) => db.delete(\"m\", k),\n },\n messageIds: {\n get: async (k) => {\n try {\n return await db.get(\"q\", k);\n } catch (error) {\n console.error(\"Error getting message IDs from cache:\", error);\n console.error(\"resetting cache...\");\n await resetCacheDB();\n return undefined;\n }\n },\n set: async (k, v) => {\n await db.put(\"q\", v, k);\n },\n del: (k) => db.delete(\"q\", k),\n },\n };\n }\n\n const m = new Map<string, LabeledMessageBase | null>();\n const q = new Map<string, CacheQueryValue>();\n\n return {\n messages: {\n get: async (k) => m.get(k),\n set: async (k, v) => void m.set(k, v),\n setMany: async (entries) => {\n for (const [k, v] of entries) m.set(k, v);\n },\n del: async (k) => void m.delete(k),\n },\n messageIds: {\n get: async (k) => q.get(k),\n set: async (k, v) => void q.set(k, v),\n del: async (k) => void q.delete(k),\n },\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["dagCborEncode", "db"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var messages_exports = {};
|
|
20
|
+
__export(messages_exports, {
|
|
21
|
+
LABELED_MESSAGE_ID_KEY: () => LABELED_MESSAGE_ID_KEY,
|
|
22
|
+
LABELED_MESSAGE_LABEL_KEY: () => LABELED_MESSAGE_LABEL_KEY,
|
|
23
|
+
LABELED_MESSAGE_MESSAGE_KEY: () => LABELED_MESSAGE_MESSAGE_KEY,
|
|
24
|
+
LabeledMessageBaseSchema: () => LabeledMessageBaseSchema,
|
|
25
|
+
MESSAGE_METADATA_KEY: () => MESSAGE_METADATA_KEY,
|
|
26
|
+
MESSAGE_OBJECT_KEY: () => MESSAGE_OBJECT_KEY,
|
|
27
|
+
MESSAGE_TAGS_KEY: () => MESSAGE_TAGS_KEY,
|
|
28
|
+
MessageBaseSchema: () => MessageBaseSchema,
|
|
29
|
+
MessageResultSchema: () => MessageResultSchema,
|
|
30
|
+
SendResponseSchema: () => SendResponseSchema,
|
|
31
|
+
TagsSchema: () => TagsSchema,
|
|
32
|
+
Uint8ArraySchema: () => Uint8ArraySchema
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(messages_exports);
|
|
35
|
+
var import_mini = require("zod/mini");
|
|
36
|
+
const GraffitiObjectSchema = (0, import_mini.strictObject)({
|
|
37
|
+
value: (0, import_mini.looseObject)({}),
|
|
38
|
+
channels: (0, import_mini.array)((0, import_mini.string)()),
|
|
39
|
+
allowed: (0, import_mini.optional)((0, import_mini.nullable)((0, import_mini.array)((0, import_mini.url)()))),
|
|
40
|
+
url: (0, import_mini.url)(),
|
|
41
|
+
actor: (0, import_mini.url)()
|
|
42
|
+
});
|
|
43
|
+
const Uint8ArraySchema = (0, import_mini.custom)(
|
|
44
|
+
(v) => v instanceof Uint8Array
|
|
45
|
+
);
|
|
46
|
+
const TagsSchema = (0, import_mini.array)(Uint8ArraySchema);
|
|
47
|
+
const MESSAGE_TAGS_KEY = "t";
|
|
48
|
+
const MESSAGE_OBJECT_KEY = "o";
|
|
49
|
+
const MESSAGE_METADATA_KEY = "m";
|
|
50
|
+
const MessageBaseSchema = (0, import_mini.strictObject)({
|
|
51
|
+
[MESSAGE_TAGS_KEY]: TagsSchema,
|
|
52
|
+
[MESSAGE_OBJECT_KEY]: GraffitiObjectSchema,
|
|
53
|
+
[MESSAGE_METADATA_KEY]: Uint8ArraySchema
|
|
54
|
+
});
|
|
55
|
+
const LABELED_MESSAGE_ID_KEY = "id";
|
|
56
|
+
const LABELED_MESSAGE_MESSAGE_KEY = "m";
|
|
57
|
+
const LABELED_MESSAGE_LABEL_KEY = "l";
|
|
58
|
+
const LabeledMessageBaseSchema = (0, import_mini.strictObject)({
|
|
59
|
+
[LABELED_MESSAGE_ID_KEY]: (0, import_mini.string)(),
|
|
60
|
+
[LABELED_MESSAGE_MESSAGE_KEY]: MessageBaseSchema,
|
|
61
|
+
[LABELED_MESSAGE_LABEL_KEY]: (0, import_mini.number)()
|
|
62
|
+
});
|
|
63
|
+
const SendResponseSchema = (0, import_mini.strictObject)({ id: (0, import_mini.string)() });
|
|
64
|
+
const MessageResultSchema = (0, import_mini.strictObject)({
|
|
65
|
+
results: (0, import_mini.array)(LabeledMessageBaseSchema),
|
|
66
|
+
hasMore: (0, import_mini.boolean)(),
|
|
67
|
+
cursor: (0, import_mini.string)()
|
|
68
|
+
});
|
|
69
|
+
//# sourceMappingURL=messages.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/1-services/4-inboxes/messages.ts"],
|
|
4
|
+
"sourcesContent": ["import type { JSONSchema, GraffitiObject } from \"@graffiti-garden/api\";\nimport {\n type infer as infer_,\n string,\n url,\n array,\n optional,\n nullable,\n strictObject,\n looseObject,\n custom,\n number,\n boolean,\n} from \"zod/mini\";\n\nconst GraffitiObjectSchema = strictObject({\n value: looseObject({}),\n channels: array(string()),\n allowed: optional(nullable(array(url()))),\n url: url(),\n actor: url(),\n});\nexport const Uint8ArraySchema = custom<Uint8Array>(\n (v): v is Uint8Array => v instanceof Uint8Array,\n);\nexport const TagsSchema = array(Uint8ArraySchema);\n\nexport const MESSAGE_TAGS_KEY = \"t\";\nexport const MESSAGE_OBJECT_KEY = \"o\";\nexport const MESSAGE_METADATA_KEY = \"m\";\nexport const MessageBaseSchema = strictObject({\n [MESSAGE_TAGS_KEY]: TagsSchema,\n [MESSAGE_OBJECT_KEY]: GraffitiObjectSchema,\n [MESSAGE_METADATA_KEY]: Uint8ArraySchema,\n});\ntype MessageBase = infer_<typeof MessageBaseSchema>;\n\nexport const LABELED_MESSAGE_ID_KEY = \"id\";\nexport const LABELED_MESSAGE_MESSAGE_KEY = \"m\";\nexport const LABELED_MESSAGE_LABEL_KEY = \"l\";\nexport const LabeledMessageBaseSchema = strictObject({\n [LABELED_MESSAGE_ID_KEY]: string(),\n [LABELED_MESSAGE_MESSAGE_KEY]: MessageBaseSchema,\n [LABELED_MESSAGE_LABEL_KEY]: number(),\n});\nexport type LabeledMessageBase = infer_<typeof LabeledMessageBaseSchema>;\n\nexport type Message<Schema extends JSONSchema> = MessageBase & {\n [MESSAGE_OBJECT_KEY]: GraffitiObject<Schema>;\n};\nexport type LabeledMessage<Schema extends JSONSchema> = LabeledMessageBase & {\n [LABELED_MESSAGE_MESSAGE_KEY]: {\n [MESSAGE_OBJECT_KEY]: GraffitiObject<Schema>;\n };\n};\n\nexport const SendResponseSchema = strictObject({ id: string() });\n\nexport const MessageResultSchema = strictObject({\n results: array(LabeledMessageBaseSchema),\n hasMore: boolean(),\n cursor: string(),\n});\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,kBAYO;AAEP,MAAM,2BAAuB,0BAAa;AAAA,EACxC,WAAO,yBAAY,CAAC,CAAC;AAAA,EACrB,cAAU,uBAAM,oBAAO,CAAC;AAAA,EACxB,aAAS,0BAAS,0BAAS,uBAAM,iBAAI,CAAC,CAAC,CAAC;AAAA,EACxC,SAAK,iBAAI;AAAA,EACT,WAAO,iBAAI;AACb,CAAC;AACM,MAAM,uBAAmB;AAAA,EAC9B,CAAC,MAAuB,aAAa;AACvC;AACO,MAAM,iBAAa,mBAAM,gBAAgB;AAEzC,MAAM,mBAAmB;AACzB,MAAM,qBAAqB;AAC3B,MAAM,uBAAuB;AAC7B,MAAM,wBAAoB,0BAAa;AAAA,EAC5C,CAAC,gBAAgB,GAAG;AAAA,EACpB,CAAC,kBAAkB,GAAG;AAAA,EACtB,CAAC,oBAAoB,GAAG;AAC1B,CAAC;AAGM,MAAM,yBAAyB;AAC/B,MAAM,8BAA8B;AACpC,MAAM,4BAA4B;AAClC,MAAM,+BAA2B,0BAAa;AAAA,EACnD,CAAC,sBAAsB,OAAG,oBAAO;AAAA,EACjC,CAAC,2BAA2B,GAAG;AAAA,EAC/B,CAAC,yBAAyB,OAAG,oBAAO;AACtC,CAAC;AAYM,MAAM,yBAAqB,0BAAa,EAAE,QAAI,oBAAO,EAAE,CAAC;AAExD,MAAM,0BAAsB,0BAAa;AAAA,EAC9C,aAAS,mBAAM,wBAAwB;AAAA,EACvC,aAAS,qBAAQ;AAAA,EACjB,YAAQ,oBAAO;AACjB,CAAC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -18,13 +18,15 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
18
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
19
|
var inboxes_tests_exports = {};
|
|
20
20
|
__export(inboxes_tests_exports, {
|
|
21
|
-
inboxTests: () => inboxTests
|
|
21
|
+
inboxTests: () => inboxTests,
|
|
22
|
+
inboxUnitTests: () => inboxUnitTests
|
|
22
23
|
});
|
|
23
24
|
module.exports = __toCommonJS(inboxes_tests_exports);
|
|
24
25
|
var import_vitest = require("vitest");
|
|
25
26
|
var import_inboxes = require("./4-inboxes");
|
|
26
27
|
var import_utilities = require("./utilities");
|
|
27
28
|
var import_utils = require("@noble/hashes/utils.js");
|
|
29
|
+
var import_api = require("@graffiti-garden/api");
|
|
28
30
|
var import_dag_cbor = require("@ipld/dag-cbor");
|
|
29
31
|
function inboxTests(inboxEndpoint, inboxToken) {
|
|
30
32
|
(0, import_vitest.describe)("Inboxes", async () => {
|
|
@@ -49,10 +51,10 @@ function inboxTests(inboxEndpoint, inboxToken) {
|
|
|
49
51
|
t: tags
|
|
50
52
|
};
|
|
51
53
|
const messageId = await inboxes.send(inboxEndpoint, sending);
|
|
52
|
-
const
|
|
53
|
-
(0, import_vitest.assert)(
|
|
54
|
-
(0, import_vitest.expect)(
|
|
55
|
-
(0, import_vitest.expect)(
|
|
54
|
+
const message2 = await inboxes.get(inboxEndpoint, messageId, inboxToken);
|
|
55
|
+
(0, import_vitest.assert)(message2 !== null);
|
|
56
|
+
(0, import_vitest.expect)(message2.m).toEqual(sending);
|
|
57
|
+
(0, import_vitest.expect)(message2.l).toEqual(0);
|
|
56
58
|
const iterator = inboxes.query(inboxEndpoint, tags, {}, inboxToken);
|
|
57
59
|
const result = await iterator.next();
|
|
58
60
|
(0, import_vitest.assert)(!result.done);
|
|
@@ -69,10 +71,10 @@ function inboxTests(inboxEndpoint, inboxToken) {
|
|
|
69
71
|
(0, import_vitest.expect)(result2.value.l).toEqual(42);
|
|
70
72
|
const endResult2 = await iterator2.next();
|
|
71
73
|
(0, import_vitest.expect)(endResult2.done).toBe(true);
|
|
72
|
-
const
|
|
73
|
-
(0, import_vitest.assert)(
|
|
74
|
-
(0, import_vitest.expect)(
|
|
75
|
-
(0, import_vitest.expect)(
|
|
74
|
+
const message22 = await inboxes.get(inboxEndpoint, messageId, inboxToken);
|
|
75
|
+
(0, import_vitest.assert)(message22 !== null);
|
|
76
|
+
(0, import_vitest.expect)(message22.m).toEqual(sending);
|
|
77
|
+
(0, import_vitest.expect)(message22.l).toEqual(42);
|
|
76
78
|
});
|
|
77
79
|
(0, import_vitest.test)("query with continue", async () => {
|
|
78
80
|
const tags = [(0, import_utils.randomBytes)(), (0, import_utils.randomBytes)()];
|
|
@@ -164,4 +166,332 @@ function inboxTests(inboxEndpoint, inboxToken) {
|
|
|
164
166
|
}, 1e5);
|
|
165
167
|
});
|
|
166
168
|
}
|
|
169
|
+
const inboxUrl = "https://example.test/inbox";
|
|
170
|
+
const message = {
|
|
171
|
+
id: "message-1",
|
|
172
|
+
m: {
|
|
173
|
+
t: [new Uint8Array([1])],
|
|
174
|
+
o: {
|
|
175
|
+
url: "https://example.test/object",
|
|
176
|
+
actor: "did:example:author",
|
|
177
|
+
channels: ["test"],
|
|
178
|
+
value: {}
|
|
179
|
+
},
|
|
180
|
+
m: new Uint8Array([2])
|
|
181
|
+
},
|
|
182
|
+
l: 0
|
|
183
|
+
};
|
|
184
|
+
function cborResponse(value) {
|
|
185
|
+
return new Response(new Uint8Array((0, import_dag_cbor.encode)(value)).slice(), {
|
|
186
|
+
headers: { "Content-Type": "application/cbor" }
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
function queryPage(results, cursor, hasMore = false) {
|
|
190
|
+
return cborResponse({ results, hasMore, cursor });
|
|
191
|
+
}
|
|
192
|
+
class TestInboxes extends import_inboxes.Inboxes {
|
|
193
|
+
async prepareCache() {
|
|
194
|
+
await this.cache;
|
|
195
|
+
}
|
|
196
|
+
async deleteCachedQuery(key) {
|
|
197
|
+
await (await this.cache).messageIds.del(key);
|
|
198
|
+
}
|
|
199
|
+
async fetchQueryBatch() {
|
|
200
|
+
const batch = await this.fetchMessageBatch(
|
|
201
|
+
"https://example.test/inbox",
|
|
202
|
+
"query",
|
|
203
|
+
void 0
|
|
204
|
+
);
|
|
205
|
+
await batch.response.arrayBuffer();
|
|
206
|
+
return batch;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function enableLockPath(request) {
|
|
210
|
+
import_vitest.vi.stubGlobal("indexedDB", {
|
|
211
|
+
open: () => {
|
|
212
|
+
const probe = {};
|
|
213
|
+
queueMicrotask(() => {
|
|
214
|
+
probe.result = { close() {
|
|
215
|
+
} };
|
|
216
|
+
probe.onsuccess?.();
|
|
217
|
+
});
|
|
218
|
+
return probe;
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
import_vitest.vi.stubGlobal("window", { navigator: { locks: { request } } });
|
|
222
|
+
}
|
|
223
|
+
function inboxUnitTests() {
|
|
224
|
+
(0, import_vitest.describe)("Inboxes unit", () => {
|
|
225
|
+
(0, import_vitest.afterEach)(() => {
|
|
226
|
+
import_vitest.vi.unstubAllGlobals();
|
|
227
|
+
import_vitest.vi.restoreAllMocks();
|
|
228
|
+
});
|
|
229
|
+
(0, import_vitest.describe)("inbox stream lock and network failures", () => {
|
|
230
|
+
(0, import_vitest.test)("backs off for five seconds after an initial request failure", async () => {
|
|
231
|
+
const failure = new Error("offline");
|
|
232
|
+
const fetch2 = import_vitest.vi.fn(() => Promise.reject(failure));
|
|
233
|
+
import_vitest.vi.stubGlobal("fetch", fetch2);
|
|
234
|
+
import_vitest.vi.spyOn(console, "error").mockImplementation(() => {
|
|
235
|
+
});
|
|
236
|
+
const realSetTimeout = globalThis.setTimeout;
|
|
237
|
+
const timer = import_vitest.vi.spyOn(globalThis, "setTimeout").mockImplementation(
|
|
238
|
+
((handler, delay, ...args) => realSetTimeout(
|
|
239
|
+
handler,
|
|
240
|
+
delay === 5e3 ? 0 : delay,
|
|
241
|
+
...args
|
|
242
|
+
))
|
|
243
|
+
);
|
|
244
|
+
await (0, import_vitest.expect)(new import_inboxes.Inboxes().query(inboxUrl, [], {}).next()).rejects.toBe(
|
|
245
|
+
failure
|
|
246
|
+
);
|
|
247
|
+
(0, import_vitest.expect)(fetch2).toHaveBeenCalledOnce();
|
|
248
|
+
(0, import_vitest.expect)(timer).toHaveBeenCalledWith(import_vitest.expect.any(Function), 5e3);
|
|
249
|
+
});
|
|
250
|
+
(0, import_vitest.test)("a rejected lock request rejects the stream", async () => {
|
|
251
|
+
const inboxes = new TestInboxes();
|
|
252
|
+
await inboxes.prepareCache();
|
|
253
|
+
const error = new Error("lock request failed");
|
|
254
|
+
enableLockPath(import_vitest.vi.fn(() => Promise.reject(error)));
|
|
255
|
+
await (0, import_vitest.expect)(
|
|
256
|
+
inboxes.query("https://example.test/inbox", [], {}).next()
|
|
257
|
+
).rejects.toBe(error);
|
|
258
|
+
});
|
|
259
|
+
(0, import_vitest.test)("waiting for another tab's lock has a deadline", async () => {
|
|
260
|
+
const inboxes = new TestInboxes();
|
|
261
|
+
await inboxes.prepareCache();
|
|
262
|
+
const timeoutError = new DOMException("timed out", "TimeoutError");
|
|
263
|
+
import_vitest.vi.spyOn(AbortSignal, "timeout").mockReturnValue(
|
|
264
|
+
AbortSignal.abort(timeoutError)
|
|
265
|
+
);
|
|
266
|
+
const request = import_vitest.vi.fn(
|
|
267
|
+
(_key, _options) => Promise.reject(timeoutError)
|
|
268
|
+
);
|
|
269
|
+
enableLockPath(request);
|
|
270
|
+
await (0, import_vitest.expect)(
|
|
271
|
+
inboxes.query("https://example.test/inbox", [], {}).next()
|
|
272
|
+
).rejects.toThrow("Timed out waiting for another tab's inbox query");
|
|
273
|
+
(0, import_vitest.expect)(request).toHaveBeenCalledOnce();
|
|
274
|
+
});
|
|
275
|
+
(0, import_vitest.test)("a stalled request is aborted", async () => {
|
|
276
|
+
const realTimeout = AbortSignal.timeout;
|
|
277
|
+
import_vitest.vi.spyOn(AbortSignal, "timeout").mockImplementation(() => realTimeout(5));
|
|
278
|
+
import_vitest.vi.stubGlobal(
|
|
279
|
+
"fetch",
|
|
280
|
+
import_vitest.vi.fn(
|
|
281
|
+
(_url, options) => new Promise((_resolve, reject) => {
|
|
282
|
+
const signal = options.signal;
|
|
283
|
+
signal.addEventListener("abort", () => reject(signal.reason), {
|
|
284
|
+
once: true
|
|
285
|
+
});
|
|
286
|
+
})
|
|
287
|
+
)
|
|
288
|
+
);
|
|
289
|
+
await (0, import_vitest.expect)(new TestInboxes().fetchQueryBatch()).rejects.toMatchObject({
|
|
290
|
+
name: "TimeoutError"
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
(0, import_vitest.test)("the deadline remains active while reading a response body", async () => {
|
|
294
|
+
const realTimeout = AbortSignal.timeout;
|
|
295
|
+
import_vitest.vi.spyOn(AbortSignal, "timeout").mockImplementation(() => realTimeout(5));
|
|
296
|
+
import_vitest.vi.stubGlobal(
|
|
297
|
+
"fetch",
|
|
298
|
+
import_vitest.vi.fn((_url, options) => {
|
|
299
|
+
const signal = options.signal;
|
|
300
|
+
const body = new ReadableStream({
|
|
301
|
+
start(controller) {
|
|
302
|
+
signal.addEventListener("abort", () => controller.error(signal.reason), {
|
|
303
|
+
once: true
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
return Promise.resolve(new Response(body));
|
|
308
|
+
})
|
|
309
|
+
);
|
|
310
|
+
await (0, import_vitest.expect)(new TestInboxes().fetchQueryBatch()).rejects.toMatchObject({
|
|
311
|
+
name: "TimeoutError"
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
(0, import_vitest.describe)("inbox query cache", () => {
|
|
316
|
+
(0, import_vitest.test)("fetches the next page while the current message is yielded", async () => {
|
|
317
|
+
const inboxes = new TestInboxes();
|
|
318
|
+
await inboxes.prepareCache();
|
|
319
|
+
let held = 0;
|
|
320
|
+
enableLockPath(import_vitest.vi.fn(async (key, _options, callback) => {
|
|
321
|
+
held++;
|
|
322
|
+
try {
|
|
323
|
+
return await callback({ name: key, mode: "exclusive" });
|
|
324
|
+
} finally {
|
|
325
|
+
held--;
|
|
326
|
+
}
|
|
327
|
+
}));
|
|
328
|
+
let prefetchAborted = false;
|
|
329
|
+
const fetch2 = import_vitest.vi.fn((_url, options) => {
|
|
330
|
+
if (fetch2.mock.calls.length === 1) {
|
|
331
|
+
return Promise.resolve(queryPage([message], "cursor-1", true));
|
|
332
|
+
}
|
|
333
|
+
return new Promise((_resolve, reject) => {
|
|
334
|
+
const signal = options.signal;
|
|
335
|
+
signal.addEventListener("abort", () => {
|
|
336
|
+
prefetchAborted = true;
|
|
337
|
+
reject(signal.reason);
|
|
338
|
+
}, { once: true });
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
import_vitest.vi.stubGlobal("fetch", fetch2);
|
|
342
|
+
const iterator = inboxes.query(inboxUrl, [new Uint8Array([1])], {});
|
|
343
|
+
(0, import_vitest.expect)((await iterator.next()).value).toEqual(message);
|
|
344
|
+
await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(fetch2).toHaveBeenCalledTimes(2));
|
|
345
|
+
(0, import_vitest.expect)(fetch2.mock.calls[1][0]).toBe(`${inboxUrl}/query?cursor=cursor-1`);
|
|
346
|
+
await iterator.return("");
|
|
347
|
+
(0, import_vitest.expect)(prefetchAborted).toBe(true);
|
|
348
|
+
await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(held).toBe(0));
|
|
349
|
+
});
|
|
350
|
+
(0, import_vitest.test)("reports a cursor that expires after the first page", async () => {
|
|
351
|
+
const fetch2 = import_vitest.vi.fn().mockResolvedValueOnce(queryPage([message], "cursor-1", true)).mockResolvedValueOnce(new Response("stale", { status: 410 }));
|
|
352
|
+
import_vitest.vi.stubGlobal("fetch", fetch2);
|
|
353
|
+
const iterator = new import_inboxes.Inboxes().query(inboxUrl, [new Uint8Array([1])], {});
|
|
354
|
+
(0, import_vitest.expect)((await iterator.next()).value).toEqual(message);
|
|
355
|
+
await (0, import_vitest.expect)(iterator.next()).rejects.toBeInstanceOf(
|
|
356
|
+
import_api.GraffitiErrorCursorExpired
|
|
357
|
+
);
|
|
358
|
+
(0, import_vitest.expect)(fetch2).toHaveBeenCalledTimes(2);
|
|
359
|
+
});
|
|
360
|
+
(0, import_vitest.test)("does not replay messages after another tab resets the cache", async () => {
|
|
361
|
+
const inboxes = new TestInboxes();
|
|
362
|
+
await inboxes.prepareCache();
|
|
363
|
+
let markPrefetchStarted;
|
|
364
|
+
const prefetchStarted = new Promise((resolve) => {
|
|
365
|
+
markPrefetchStarted = resolve;
|
|
366
|
+
});
|
|
367
|
+
let releasePrefetch;
|
|
368
|
+
const pause = new Promise((resolve) => {
|
|
369
|
+
releasePrefetch = resolve;
|
|
370
|
+
});
|
|
371
|
+
let lockCalls = 0;
|
|
372
|
+
let cacheKey = "";
|
|
373
|
+
enableLockPath(import_vitest.vi.fn(async (name, _options, work) => {
|
|
374
|
+
cacheKey = name.slice("graffiti:inbox:".length);
|
|
375
|
+
if (++lockCalls === 2) {
|
|
376
|
+
markPrefetchStarted();
|
|
377
|
+
await pause;
|
|
378
|
+
}
|
|
379
|
+
return work({ name, mode: "exclusive" });
|
|
380
|
+
}));
|
|
381
|
+
import_vitest.vi.stubGlobal("fetch", import_vitest.vi.fn(async () => queryPage([message], "cursor-1", true)));
|
|
382
|
+
const iterator = inboxes.query(inboxUrl, [new Uint8Array([1])], {});
|
|
383
|
+
(0, import_vitest.expect)((await iterator.next()).value).toEqual(message);
|
|
384
|
+
await prefetchStarted;
|
|
385
|
+
await inboxes.deleteCachedQuery(cacheKey);
|
|
386
|
+
releasePrefetch();
|
|
387
|
+
await (0, import_vitest.expect)(iterator.next()).rejects.toBeInstanceOf(
|
|
388
|
+
import_api.GraffitiErrorCursorExpired
|
|
389
|
+
);
|
|
390
|
+
});
|
|
391
|
+
(0, import_vitest.test)("a later query reads messages from the cached server cursor", async () => {
|
|
392
|
+
const fetch2 = import_vitest.vi.fn().mockResolvedValueOnce(queryPage([message], "cursor-1")).mockResolvedValueOnce(queryPage([], "cursor-2"));
|
|
393
|
+
import_vitest.vi.stubGlobal("fetch", fetch2);
|
|
394
|
+
const inboxes = new import_inboxes.Inboxes();
|
|
395
|
+
const tags = [new Uint8Array([1])];
|
|
396
|
+
const first = inboxes.query(inboxUrl, tags, {});
|
|
397
|
+
(0, import_vitest.expect)((await first.next()).value).toEqual(message);
|
|
398
|
+
(0, import_vitest.expect)((await first.next()).done).toBe(true);
|
|
399
|
+
const second = inboxes.query(inboxUrl, tags, {});
|
|
400
|
+
(0, import_vitest.expect)((await second.next()).value).toEqual(message);
|
|
401
|
+
(0, import_vitest.expect)((await second.next()).done).toBe(true);
|
|
402
|
+
(0, import_vitest.expect)(fetch2).toHaveBeenCalledTimes(2);
|
|
403
|
+
(0, import_vitest.expect)(fetch2.mock.calls[1][0]).toBe(`${inboxUrl}/query?cursor=cursor-1`);
|
|
404
|
+
});
|
|
405
|
+
(0, import_vitest.test)("does not hold the query lock while a message is yielded", async () => {
|
|
406
|
+
const inboxes = new TestInboxes();
|
|
407
|
+
await inboxes.prepareCache();
|
|
408
|
+
let held = false;
|
|
409
|
+
enableLockPath(import_vitest.vi.fn(async (key, _options, callback) => {
|
|
410
|
+
held = true;
|
|
411
|
+
try {
|
|
412
|
+
return await callback({ name: key, mode: "exclusive" });
|
|
413
|
+
} finally {
|
|
414
|
+
held = false;
|
|
415
|
+
}
|
|
416
|
+
}));
|
|
417
|
+
import_vitest.vi.stubGlobal("fetch", import_vitest.vi.fn(async () => queryPage([message], "cursor-1")));
|
|
418
|
+
const iterator = inboxes.query(inboxUrl, [new Uint8Array([1])], {});
|
|
419
|
+
try {
|
|
420
|
+
(0, import_vitest.expect)((await iterator.next()).value).toEqual(message);
|
|
421
|
+
(0, import_vitest.expect)(held).toBe(false);
|
|
422
|
+
} finally {
|
|
423
|
+
await iterator.return("");
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
(0, import_vitest.test)("closing a yielded query leaves no lock held", async () => {
|
|
427
|
+
const inboxes = new TestInboxes();
|
|
428
|
+
await inboxes.prepareCache();
|
|
429
|
+
let held = false;
|
|
430
|
+
enableLockPath(import_vitest.vi.fn(async (key, _options, callback) => {
|
|
431
|
+
held = true;
|
|
432
|
+
try {
|
|
433
|
+
return await callback({ name: key, mode: "exclusive" });
|
|
434
|
+
} finally {
|
|
435
|
+
held = false;
|
|
436
|
+
}
|
|
437
|
+
}));
|
|
438
|
+
import_vitest.vi.stubGlobal("fetch", import_vitest.vi.fn(async () => queryPage([message], "cursor-1")));
|
|
439
|
+
const iterator = inboxes.query(inboxUrl, [new Uint8Array([1])], {});
|
|
440
|
+
(0, import_vitest.expect)((await iterator.next()).value).toEqual(message);
|
|
441
|
+
(0, import_vitest.expect)(held).toBe(false);
|
|
442
|
+
await iterator.return("");
|
|
443
|
+
(0, import_vitest.expect)(held).toBe(false);
|
|
444
|
+
});
|
|
445
|
+
(0, import_vitest.test)("concurrent queries reuse a page fetched under the lock", async () => {
|
|
446
|
+
const inboxes = new TestInboxes();
|
|
447
|
+
await inboxes.prepareCache();
|
|
448
|
+
let releasePrevious = Promise.resolve();
|
|
449
|
+
let requests = 0;
|
|
450
|
+
let bothRequested;
|
|
451
|
+
const ready = new Promise((resolve) => {
|
|
452
|
+
bothRequested = resolve;
|
|
453
|
+
});
|
|
454
|
+
enableLockPath(import_vitest.vi.fn(async (key, _options, callback) => {
|
|
455
|
+
requests++;
|
|
456
|
+
if (requests === 2) bothRequested();
|
|
457
|
+
const previous = releasePrevious;
|
|
458
|
+
let release;
|
|
459
|
+
releasePrevious = new Promise((resolve) => {
|
|
460
|
+
release = resolve;
|
|
461
|
+
});
|
|
462
|
+
await previous;
|
|
463
|
+
try {
|
|
464
|
+
return await callback({ name: key, mode: "exclusive" });
|
|
465
|
+
} finally {
|
|
466
|
+
release();
|
|
467
|
+
}
|
|
468
|
+
}));
|
|
469
|
+
const nextMessage = { ...message, id: "message-2" };
|
|
470
|
+
const fetch2 = import_vitest.vi.fn(async () => {
|
|
471
|
+
await ready;
|
|
472
|
+
return fetch2.mock.calls.length === 1 ? queryPage([message], "cursor-1", true) : queryPage([nextMessage], "cursor-2");
|
|
473
|
+
});
|
|
474
|
+
import_vitest.vi.stubGlobal("fetch", fetch2);
|
|
475
|
+
const tags = [new Uint8Array([1])];
|
|
476
|
+
const first = inboxes.query(inboxUrl, tags, {});
|
|
477
|
+
const second = inboxes.query(inboxUrl, tags, {});
|
|
478
|
+
const [firstResult, secondResult] = await Promise.all([
|
|
479
|
+
first.next(),
|
|
480
|
+
second.next()
|
|
481
|
+
]);
|
|
482
|
+
(0, import_vitest.expect)(firstResult.value).toEqual(message);
|
|
483
|
+
(0, import_vitest.expect)(secondResult.value).toEqual(message);
|
|
484
|
+
const [firstNext, secondNext] = await Promise.all([
|
|
485
|
+
first.next(),
|
|
486
|
+
second.next()
|
|
487
|
+
]);
|
|
488
|
+
(0, import_vitest.expect)(firstNext.value).toEqual(nextMessage);
|
|
489
|
+
(0, import_vitest.expect)(secondNext.value).toEqual(nextMessage);
|
|
490
|
+
(0, import_vitest.expect)(fetch2).toHaveBeenCalledTimes(2);
|
|
491
|
+
(0, import_vitest.expect)((await first.next()).done).toBe(true);
|
|
492
|
+
(0, import_vitest.expect)((await second.next()).done).toBe(true);
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
}
|
|
167
497
|
//# sourceMappingURL=4-inboxes-tests.js.map
|