@graffiti-garden/implementation-decentralized 0.0.1

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.
Files changed (193) hide show
  1. package/LICENSE +674 -0
  2. package/dist/1-services/1-authorization.d.ts +37 -0
  3. package/dist/1-services/1-authorization.d.ts.map +1 -0
  4. package/dist/1-services/2-dids-tests.d.ts +2 -0
  5. package/dist/1-services/2-dids-tests.d.ts.map +1 -0
  6. package/dist/1-services/2-dids.d.ts +9 -0
  7. package/dist/1-services/2-dids.d.ts.map +1 -0
  8. package/dist/1-services/3-storage-buckets-tests.d.ts +2 -0
  9. package/dist/1-services/3-storage-buckets-tests.d.ts.map +1 -0
  10. package/dist/1-services/3-storage-buckets.d.ts +11 -0
  11. package/dist/1-services/3-storage-buckets.d.ts.map +1 -0
  12. package/dist/1-services/4-inboxes-tests.d.ts +2 -0
  13. package/dist/1-services/4-inboxes-tests.d.ts.map +1 -0
  14. package/dist/1-services/4-inboxes.d.ts +87 -0
  15. package/dist/1-services/4-inboxes.d.ts.map +1 -0
  16. package/dist/1-services/utilities.d.ts +7 -0
  17. package/dist/1-services/utilities.d.ts.map +1 -0
  18. package/dist/2-primitives/1-string-encoding-tests.d.ts +2 -0
  19. package/dist/2-primitives/1-string-encoding-tests.d.ts.map +1 -0
  20. package/dist/2-primitives/1-string-encoding.d.ts +6 -0
  21. package/dist/2-primitives/1-string-encoding.d.ts.map +1 -0
  22. package/dist/2-primitives/2-content-addresses-tests.d.ts +2 -0
  23. package/dist/2-primitives/2-content-addresses-tests.d.ts.map +1 -0
  24. package/dist/2-primitives/2-content-addresses.d.ts +8 -0
  25. package/dist/2-primitives/2-content-addresses.d.ts.map +1 -0
  26. package/dist/2-primitives/3-channel-attestations-tests.d.ts +2 -0
  27. package/dist/2-primitives/3-channel-attestations-tests.d.ts.map +1 -0
  28. package/dist/2-primitives/3-channel-attestations.d.ts +13 -0
  29. package/dist/2-primitives/3-channel-attestations.d.ts.map +1 -0
  30. package/dist/2-primitives/4-allowed-attestations-tests.d.ts +2 -0
  31. package/dist/2-primitives/4-allowed-attestations-tests.d.ts.map +1 -0
  32. package/dist/2-primitives/4-allowed-attestations.d.ts +9 -0
  33. package/dist/2-primitives/4-allowed-attestations.d.ts.map +1 -0
  34. package/dist/3-protocol/1-sessions.d.ts +81 -0
  35. package/dist/3-protocol/1-sessions.d.ts.map +1 -0
  36. package/dist/3-protocol/2-handles-tests.d.ts +2 -0
  37. package/dist/3-protocol/2-handles-tests.d.ts.map +1 -0
  38. package/dist/3-protocol/2-handles.d.ts +13 -0
  39. package/dist/3-protocol/2-handles.d.ts.map +1 -0
  40. package/dist/3-protocol/3-object-encoding-tests.d.ts +2 -0
  41. package/dist/3-protocol/3-object-encoding-tests.d.ts.map +1 -0
  42. package/dist/3-protocol/3-object-encoding.d.ts +43 -0
  43. package/dist/3-protocol/3-object-encoding.d.ts.map +1 -0
  44. package/dist/3-protocol/4-graffiti.d.ts +79 -0
  45. package/dist/3-protocol/4-graffiti.d.ts.map +1 -0
  46. package/dist/3-protocol/login-dialog.html.d.ts +2 -0
  47. package/dist/3-protocol/login-dialog.html.d.ts.map +1 -0
  48. package/dist/browser/ajv-QBSREQSI.js +9 -0
  49. package/dist/browser/ajv-QBSREQSI.js.map +7 -0
  50. package/dist/browser/build-BXWPS7VK.js +2 -0
  51. package/dist/browser/build-BXWPS7VK.js.map +7 -0
  52. package/dist/browser/chunk-RFBBAUMM.js +2 -0
  53. package/dist/browser/chunk-RFBBAUMM.js.map +7 -0
  54. package/dist/browser/graffiti-KV3G3O72-URO7SJIJ.js +2 -0
  55. package/dist/browser/graffiti-KV3G3O72-URO7SJIJ.js.map +7 -0
  56. package/dist/browser/index.js +16 -0
  57. package/dist/browser/index.js.map +7 -0
  58. package/dist/browser/login-dialog.html-XUWYDNNI.js +44 -0
  59. package/dist/browser/login-dialog.html-XUWYDNNI.js.map +7 -0
  60. package/dist/browser/rock-salt-LI7DAH66-KPFEBIBO.js +2 -0
  61. package/dist/browser/rock-salt-LI7DAH66-KPFEBIBO.js.map +7 -0
  62. package/dist/browser/style-YUTCEBZV-RWYJV575.js +287 -0
  63. package/dist/browser/style-YUTCEBZV-RWYJV575.js.map +7 -0
  64. package/dist/cjs/1-services/1-authorization.js +317 -0
  65. package/dist/cjs/1-services/1-authorization.js.map +7 -0
  66. package/dist/cjs/1-services/2-dids-tests.js +44 -0
  67. package/dist/cjs/1-services/2-dids-tests.js.map +7 -0
  68. package/dist/cjs/1-services/2-dids.js +47 -0
  69. package/dist/cjs/1-services/2-dids.js.map +7 -0
  70. package/dist/cjs/1-services/3-storage-buckets-tests.js +123 -0
  71. package/dist/cjs/1-services/3-storage-buckets-tests.js.map +7 -0
  72. package/dist/cjs/1-services/3-storage-buckets.js +148 -0
  73. package/dist/cjs/1-services/3-storage-buckets.js.map +7 -0
  74. package/dist/cjs/1-services/4-inboxes-tests.js +145 -0
  75. package/dist/cjs/1-services/4-inboxes-tests.js.map +7 -0
  76. package/dist/cjs/1-services/4-inboxes.js +539 -0
  77. package/dist/cjs/1-services/4-inboxes.js.map +7 -0
  78. package/dist/cjs/1-services/utilities.js +75 -0
  79. package/dist/cjs/1-services/utilities.js.map +7 -0
  80. package/dist/cjs/2-primitives/1-string-encoding-tests.js +50 -0
  81. package/dist/cjs/2-primitives/1-string-encoding-tests.js.map +7 -0
  82. package/dist/cjs/2-primitives/1-string-encoding.js +46 -0
  83. package/dist/cjs/2-primitives/1-string-encoding.js.map +7 -0
  84. package/dist/cjs/2-primitives/2-content-addresses-tests.js +62 -0
  85. package/dist/cjs/2-primitives/2-content-addresses-tests.js.map +7 -0
  86. package/dist/cjs/2-primitives/2-content-addresses.js +53 -0
  87. package/dist/cjs/2-primitives/2-content-addresses.js.map +7 -0
  88. package/dist/cjs/2-primitives/3-channel-attestations-tests.js +130 -0
  89. package/dist/cjs/2-primitives/3-channel-attestations-tests.js.map +7 -0
  90. package/dist/cjs/2-primitives/3-channel-attestations.js +84 -0
  91. package/dist/cjs/2-primitives/3-channel-attestations.js.map +7 -0
  92. package/dist/cjs/2-primitives/4-allowed-attestations-tests.js +96 -0
  93. package/dist/cjs/2-primitives/4-allowed-attestations-tests.js.map +7 -0
  94. package/dist/cjs/2-primitives/4-allowed-attestations.js +68 -0
  95. package/dist/cjs/2-primitives/4-allowed-attestations.js.map +7 -0
  96. package/dist/cjs/3-protocol/1-sessions.js +473 -0
  97. package/dist/cjs/3-protocol/1-sessions.js.map +7 -0
  98. package/dist/cjs/3-protocol/2-handles-tests.js +39 -0
  99. package/dist/cjs/3-protocol/2-handles-tests.js.map +7 -0
  100. package/dist/cjs/3-protocol/2-handles.js +65 -0
  101. package/dist/cjs/3-protocol/2-handles.js.map +7 -0
  102. package/dist/cjs/3-protocol/3-object-encoding-tests.js +253 -0
  103. package/dist/cjs/3-protocol/3-object-encoding-tests.js.map +7 -0
  104. package/dist/cjs/3-protocol/3-object-encoding.js +287 -0
  105. package/dist/cjs/3-protocol/3-object-encoding.js.map +7 -0
  106. package/dist/cjs/3-protocol/4-graffiti.js +937 -0
  107. package/dist/cjs/3-protocol/4-graffiti.js.map +7 -0
  108. package/dist/cjs/3-protocol/login-dialog.html.js +67 -0
  109. package/dist/cjs/3-protocol/login-dialog.html.js.map +7 -0
  110. package/dist/cjs/index.js +32 -0
  111. package/dist/cjs/index.js.map +7 -0
  112. package/dist/cjs/index.spec.js +130 -0
  113. package/dist/cjs/index.spec.js.map +7 -0
  114. package/dist/esm/1-services/1-authorization.js +304 -0
  115. package/dist/esm/1-services/1-authorization.js.map +7 -0
  116. package/dist/esm/1-services/2-dids-tests.js +24 -0
  117. package/dist/esm/1-services/2-dids-tests.js.map +7 -0
  118. package/dist/esm/1-services/2-dids.js +27 -0
  119. package/dist/esm/1-services/2-dids.js.map +7 -0
  120. package/dist/esm/1-services/3-storage-buckets-tests.js +103 -0
  121. package/dist/esm/1-services/3-storage-buckets-tests.js.map +7 -0
  122. package/dist/esm/1-services/3-storage-buckets.js +132 -0
  123. package/dist/esm/1-services/3-storage-buckets.js.map +7 -0
  124. package/dist/esm/1-services/4-inboxes-tests.js +125 -0
  125. package/dist/esm/1-services/4-inboxes-tests.js.map +7 -0
  126. package/dist/esm/1-services/4-inboxes.js +533 -0
  127. package/dist/esm/1-services/4-inboxes.js.map +7 -0
  128. package/dist/esm/1-services/utilities.js +60 -0
  129. package/dist/esm/1-services/utilities.js.map +7 -0
  130. package/dist/esm/2-primitives/1-string-encoding-tests.js +33 -0
  131. package/dist/esm/2-primitives/1-string-encoding-tests.js.map +7 -0
  132. package/dist/esm/2-primitives/1-string-encoding.js +26 -0
  133. package/dist/esm/2-primitives/1-string-encoding.js.map +7 -0
  134. package/dist/esm/2-primitives/2-content-addresses-tests.js +45 -0
  135. package/dist/esm/2-primitives/2-content-addresses-tests.js.map +7 -0
  136. package/dist/esm/2-primitives/2-content-addresses.js +33 -0
  137. package/dist/esm/2-primitives/2-content-addresses.js.map +7 -0
  138. package/dist/esm/2-primitives/3-channel-attestations-tests.js +116 -0
  139. package/dist/esm/2-primitives/3-channel-attestations-tests.js.map +7 -0
  140. package/dist/esm/2-primitives/3-channel-attestations.js +69 -0
  141. package/dist/esm/2-primitives/3-channel-attestations.js.map +7 -0
  142. package/dist/esm/2-primitives/4-allowed-attestations-tests.js +82 -0
  143. package/dist/esm/2-primitives/4-allowed-attestations-tests.js.map +7 -0
  144. package/dist/esm/2-primitives/4-allowed-attestations.js +51 -0
  145. package/dist/esm/2-primitives/4-allowed-attestations.js.map +7 -0
  146. package/dist/esm/3-protocol/1-sessions.js +465 -0
  147. package/dist/esm/3-protocol/1-sessions.js.map +7 -0
  148. package/dist/esm/3-protocol/2-handles-tests.js +19 -0
  149. package/dist/esm/3-protocol/2-handles-tests.js.map +7 -0
  150. package/dist/esm/3-protocol/2-handles.js +45 -0
  151. package/dist/esm/3-protocol/2-handles.js.map +7 -0
  152. package/dist/esm/3-protocol/3-object-encoding-tests.js +248 -0
  153. package/dist/esm/3-protocol/3-object-encoding-tests.js.map +7 -0
  154. package/dist/esm/3-protocol/3-object-encoding.js +280 -0
  155. package/dist/esm/3-protocol/3-object-encoding.js.map +7 -0
  156. package/dist/esm/3-protocol/4-graffiti.js +957 -0
  157. package/dist/esm/3-protocol/4-graffiti.js.map +7 -0
  158. package/dist/esm/3-protocol/login-dialog.html.js +47 -0
  159. package/dist/esm/3-protocol/login-dialog.html.js.map +7 -0
  160. package/dist/esm/index.js +14 -0
  161. package/dist/esm/index.js.map +7 -0
  162. package/dist/esm/index.spec.js +133 -0
  163. package/dist/esm/index.spec.js.map +7 -0
  164. package/dist/index.d.ts +10 -0
  165. package/dist/index.d.ts.map +1 -0
  166. package/dist/index.spec.d.ts +2 -0
  167. package/dist/index.spec.d.ts.map +1 -0
  168. package/package.json +62 -0
  169. package/src/1-services/1-authorization.ts +399 -0
  170. package/src/1-services/2-dids-tests.ts +24 -0
  171. package/src/1-services/2-dids.ts +30 -0
  172. package/src/1-services/3-storage-buckets-tests.ts +121 -0
  173. package/src/1-services/3-storage-buckets.ts +183 -0
  174. package/src/1-services/4-inboxes-tests.ts +154 -0
  175. package/src/1-services/4-inboxes.ts +722 -0
  176. package/src/1-services/utilities.ts +65 -0
  177. package/src/2-primitives/1-string-encoding-tests.ts +33 -0
  178. package/src/2-primitives/1-string-encoding.ts +33 -0
  179. package/src/2-primitives/2-content-addresses-tests.ts +46 -0
  180. package/src/2-primitives/2-content-addresses.ts +42 -0
  181. package/src/2-primitives/3-channel-attestations-tests.ts +125 -0
  182. package/src/2-primitives/3-channel-attestations.ts +95 -0
  183. package/src/2-primitives/4-allowed-attestations-tests.ts +86 -0
  184. package/src/2-primitives/4-allowed-attestations.ts +69 -0
  185. package/src/3-protocol/1-sessions.ts +601 -0
  186. package/src/3-protocol/2-handles-tests.ts +17 -0
  187. package/src/3-protocol/2-handles.ts +60 -0
  188. package/src/3-protocol/3-object-encoding-tests.ts +269 -0
  189. package/src/3-protocol/3-object-encoding.ts +396 -0
  190. package/src/3-protocol/4-graffiti.ts +1265 -0
  191. package/src/3-protocol/login-dialog.html.ts +43 -0
  192. package/src/index.spec.ts +158 -0
  193. package/src/index.ts +16 -0
@@ -0,0 +1,722 @@
1
+ import type { JSONSchema, GraffitiObject } from "@graffiti-garden/api";
2
+ import {
3
+ getAuthorizationEndpoint,
4
+ fetchWithErrorHandling,
5
+ verifyHTTPSEndpoint,
6
+ } from "./utilities";
7
+ import {
8
+ compileGraffitiObjectSchema,
9
+ GraffitiErrorCursorExpired,
10
+ } from "@graffiti-garden/api";
11
+ import {
12
+ encode as dagCborEncode,
13
+ decode as dagCborDecode,
14
+ } from "@ipld/dag-cbor";
15
+ import {
16
+ type infer as infer_,
17
+ string,
18
+ url,
19
+ array,
20
+ optional,
21
+ nullable,
22
+ strictObject,
23
+ looseObject,
24
+ nonnegative,
25
+ int,
26
+ boolean,
27
+ custom,
28
+ number,
29
+ union,
30
+ } from "zod/mini";
31
+
32
+ export class Inboxes {
33
+ getAuthorizationEndpoint = getAuthorizationEndpoint;
34
+ protected cache_: Promise<Cache> | null = null;
35
+ protected get cache() {
36
+ if (!this.cache_) {
37
+ this.cache_ = createCache();
38
+ }
39
+ return this.cache_;
40
+ }
41
+
42
+ async send(inboxUrl: string, message: Message<{}>): Promise<string> {
43
+ verifyHTTPSEndpoint(inboxUrl);
44
+ const url = `${inboxUrl}/send`;
45
+
46
+ const response = await fetchWithErrorHandling(url, {
47
+ method: "PUT",
48
+ headers: {
49
+ "Content-Type": "application/cbor",
50
+ },
51
+ body: new Uint8Array(dagCborEncode({ m: message })),
52
+ });
53
+
54
+ const blob = await response.blob();
55
+ const cbor = dagCborDecode(await blob.arrayBuffer());
56
+ const parsed = SendResponseSchema.parse(cbor);
57
+ return parsed.id;
58
+ }
59
+
60
+ async get(
61
+ inboxUrl: string,
62
+ messageId: string,
63
+ inboxToken?: string | null,
64
+ ): Promise<LabeledMessageBase> {
65
+ const messageCacheKey = getMessageCacheKey(inboxUrl, messageId);
66
+ const cache = await this.cache;
67
+ const cached = await cache.messages.get(messageCacheKey);
68
+ if (cached) return cached;
69
+
70
+ const url = `${inboxUrl}/message/${messageId}`;
71
+ const response = await fetchWithErrorHandling(url, {
72
+ method: "GET",
73
+ headers: {
74
+ ...(inboxToken
75
+ ? {
76
+ Authorization: `Bearer ${inboxToken}`,
77
+ }
78
+ : {}),
79
+ },
80
+ });
81
+
82
+ const blob = await response.blob();
83
+ const cbor = dagCborDecode(await blob.arrayBuffer());
84
+ const parsed = LabeledMessageBaseSchema.parse(cbor);
85
+
86
+ await cache.messages.set(messageCacheKey, parsed);
87
+ return parsed;
88
+ }
89
+
90
+ async label(
91
+ inboxUrl: string,
92
+ messageId: string,
93
+ label: number,
94
+ inboxToken?: string | null,
95
+ ): Promise<void> {
96
+ verifyHTTPSEndpoint(inboxUrl);
97
+
98
+ if (inboxToken) {
99
+ const url = `${inboxUrl}/label/${messageId}`;
100
+
101
+ await fetchWithErrorHandling(url, {
102
+ method: "PUT",
103
+ headers: {
104
+ "Content-Type": "application/cbor",
105
+ Authorization: `Bearer ${inboxToken}`,
106
+ },
107
+ body: new Uint8Array(dagCborEncode({ l: label })),
108
+ });
109
+ }
110
+
111
+ // Update the cache, even if no token.
112
+ // Therefore people not logged in do not need to
113
+ // repeatedly re-validate objects.
114
+ const cache = await this.cache;
115
+ const messageCacheKey = getMessageCacheKey(inboxUrl, messageId);
116
+ const result = await cache.messages.get(messageCacheKey);
117
+ if (result) {
118
+ await cache.messages.set(messageCacheKey, {
119
+ ...result,
120
+ l: label,
121
+ });
122
+ }
123
+ }
124
+
125
+ protected async fetchMessageBatch(
126
+ inboxUrl: string,
127
+ type: "query" | "export",
128
+ body: Uint8Array<ArrayBuffer> | undefined,
129
+ inboxToken?: string | null,
130
+ cursor?: string,
131
+ ) {
132
+ const response = await fetchWithErrorHandling(
133
+ `${inboxUrl}/${type}${cursor ? `?cursor=${encodeURIComponent(cursor)}` : ""}`,
134
+ {
135
+ method: "POST",
136
+ headers: {
137
+ "Content-Type": "application/cbor",
138
+ ...(inboxToken
139
+ ? {
140
+ Authorization: `Bearer ${inboxToken}`,
141
+ }
142
+ : {}),
143
+ },
144
+ body,
145
+ },
146
+ );
147
+ const retryAfterHeader = response.headers.get("Retry-After");
148
+ const retryAfter = retryAfterHeader
149
+ ? parseInt(retryAfterHeader)
150
+ : undefined;
151
+
152
+ const waitTil =
153
+ retryAfter && Number.isFinite(retryAfter)
154
+ ? Date.now() + retryAfter * 1000
155
+ : undefined;
156
+
157
+ return { response, waitTil };
158
+ }
159
+
160
+ protected async *yieldFromCache(
161
+ cache: Cache,
162
+ inboxUrl: string,
163
+ messageIdsCacheKey: string,
164
+ cachedMessageIds: CacheQueryValue,
165
+ cacheNumSeen: number = 0,
166
+ ): AsyncGenerator<LabeledMessageBase> {
167
+ // Filter out all messageIds before
168
+ // the number already seen
169
+ const messageIds = cachedMessageIds.messageIds.slice(cacheNumSeen);
170
+
171
+ // Get all the messages pointed to in the cache
172
+ const messages = await Promise.all(
173
+ messageIds.map(async (id) => {
174
+ const message = await cache.messages.get(
175
+ getMessageCacheKey(inboxUrl, id),
176
+ );
177
+ if (!message) {
178
+ // Something is very wrong with the cache,
179
+ // it refers to message IDs that are not cached
180
+ try {
181
+ await cache.messageIds.del(messageIdsCacheKey);
182
+ } catch {}
183
+ throw new Error("Cache out of sync - perhaps clear browser storage");
184
+ }
185
+ return message;
186
+ }),
187
+ );
188
+
189
+ yield* messages;
190
+ }
191
+
192
+ protected async *lockedMessageStreamer<Schema extends JSONSchema>(
193
+ ...args: Parameters<typeof this.messageStreamer<Schema>>
194
+ ): MessageStream<Schema> {
195
+ if (typeof window === "undefined" || !(await canUseIDB())) {
196
+ // TODO: implement locking in node as well, but not
197
+ // high priority since most use will be in browser
198
+ const streamer = this.messageStreamer<Schema>(...args);
199
+ while (true) {
200
+ const next = await streamer.next();
201
+ if (next.done) return next.value;
202
+ yield next.value;
203
+ }
204
+ }
205
+
206
+ // Request the lock
207
+ const messageIdsCacheKey = await args[0];
208
+ const lockKey = `graffiti:inbox:${messageIdsCacheKey}`;
209
+ let releaseLock = () => {};
210
+ let hasLock: boolean = false;
211
+ await new Promise<void>((resolvehasLock) => {
212
+ window.navigator.locks.request(
213
+ lockKey,
214
+ {
215
+ mode: "exclusive",
216
+ ifAvailable: true,
217
+ },
218
+ async (lock) => {
219
+ // Immediately return whether we
220
+ // acquired the lock or not
221
+ hasLock = !!lock;
222
+ resolvehasLock();
223
+
224
+ // Then wait for the release to be called
225
+ await new Promise<void>((r) => (releaseLock = r));
226
+ },
227
+ );
228
+ });
229
+ if (hasLock) {
230
+ // If we have the lock, simply proceed with the regular streamer
231
+ try {
232
+ const streamer = this.messageStreamer<Schema>(...args);
233
+ while (true) {
234
+ const next = await streamer.next();
235
+ if (next.done) return next.value;
236
+ yield next.value;
237
+ }
238
+ } finally {
239
+ // Release the lock when all done
240
+ releaseLock();
241
+ }
242
+ }
243
+
244
+ // Someone else has the lock,
245
+ // so wait until the lock is released,
246
+ // then just return from the cache
247
+ releaseLock();
248
+ await window.navigator.locks.request(lockKey, () => {});
249
+
250
+ // TODO: the arguments here are brittle
251
+ // at some point, refactor things
252
+ const inboxUrl = args[1];
253
+ const objectSchema = args[5] ?? {};
254
+ const cacheVersion = args[6];
255
+ const cacheNumSeen = args[7];
256
+
257
+ const cache = await this.cache;
258
+ const cachedMessageIds = await cache.messageIds.get(messageIdsCacheKey);
259
+ if (!cachedMessageIds) {
260
+ throw new Error("Cache not found");
261
+ }
262
+ if (
263
+ cacheVersion !== undefined &&
264
+ cacheVersion !== cachedMessageIds.version
265
+ ) {
266
+ throw new GraffitiErrorCursorExpired("Cursor is stale");
267
+ }
268
+
269
+ const iterator = this.yieldFromCache(
270
+ cache,
271
+ inboxUrl,
272
+ messageIdsCacheKey,
273
+ cachedMessageIds,
274
+ cacheNumSeen,
275
+ );
276
+ for await (const m of iterator) yield m as LabeledMessage<Schema>;
277
+
278
+ const outputCursor: infer_<typeof CursorSchema> = {
279
+ numSeen: cachedMessageIds.messageIds.length,
280
+ version: cachedMessageIds.version,
281
+ messageIdsCacheKey,
282
+ objectSchema,
283
+ };
284
+
285
+ return JSON.stringify(outputCursor);
286
+ }
287
+
288
+ protected async *messageStreamer<Schema extends JSONSchema>(
289
+ messageIdsCacheKey_: Promise<string>,
290
+ inboxUrl: string,
291
+ type: "export" | "query",
292
+ body: Uint8Array<ArrayBuffer> | undefined,
293
+ inboxToken?: string | null,
294
+ objectSchema: Schema = {} as Schema,
295
+ cacheVersion?: string,
296
+ cacheNumSeen: number = 0,
297
+ ): MessageStream<Schema> {
298
+ const validator = await compileGraffitiObjectSchema(objectSchema);
299
+ const messageIdsCacheKey = await messageIdsCacheKey_;
300
+ const cache = await this.cache;
301
+
302
+ let cachedMessageIds = await cache.messageIds.get(messageIdsCacheKey);
303
+ if (
304
+ cacheVersion !== undefined &&
305
+ cacheVersion !== cachedMessageIds?.version
306
+ ) {
307
+ throw new GraffitiErrorCursorExpired("Cursor is stale");
308
+ }
309
+
310
+ // If we are rate-limited, wait
311
+ let waitTil = cachedMessageIds?.waitTil;
312
+ await waitFor(waitTil);
313
+
314
+ // See if the cursor is still active by
315
+ // requesting an initial batch of messages
316
+ const cachedCursor = cachedMessageIds?.cursor;
317
+ let firstResponse: Response | undefined = undefined;
318
+ try {
319
+ const out = await this.fetchMessageBatch(
320
+ inboxUrl,
321
+ type,
322
+ body,
323
+ inboxToken,
324
+ cachedCursor,
325
+ );
326
+ firstResponse = out.response;
327
+ waitTil = out.waitTil;
328
+ } catch (e) {
329
+ if (!(e instanceof GraffitiErrorCursorExpired && cachedCursor)) {
330
+ console.error(
331
+ "Unexpected error in stream, waiting 5 seconds before continuing...",
332
+ );
333
+ await new Promise((resolve) => setTimeout(resolve, 5000));
334
+ throw e;
335
+ }
336
+
337
+ // The cursor is stale
338
+ await cache.messageIds.del(messageIdsCacheKey);
339
+ if (cacheVersion === undefined) {
340
+ // The query is not a continuation
341
+ // so we can effectively ignore the error
342
+ cachedMessageIds = undefined;
343
+ } else {
344
+ // Otherwise propogate it up so the
345
+ // consumer can clear their message history
346
+ throw e;
347
+ }
348
+ }
349
+
350
+ if (firstResponse !== undefined && cachedMessageIds) {
351
+ // Cursor is valid! Yield from the cache
352
+ const iterator = this.yieldFromCache(
353
+ cache,
354
+ inboxUrl,
355
+ messageIdsCacheKey,
356
+ cachedMessageIds,
357
+ cacheNumSeen,
358
+ );
359
+ for await (const m of iterator) yield m as LabeledMessage<Schema>;
360
+ }
361
+
362
+ if (firstResponse === undefined) {
363
+ // The cursor was stale: try again
364
+ const out = await this.fetchMessageBatch(
365
+ inboxUrl,
366
+ type,
367
+ body,
368
+ inboxToken,
369
+ );
370
+ firstResponse = out.response;
371
+ waitTil = out.waitTil;
372
+ }
373
+
374
+ // Continue streaming results
375
+ let response = firstResponse;
376
+ let cursor: string;
377
+ const version = cachedMessageIds?.version ?? crypto.randomUUID();
378
+ let messageIds = cachedMessageIds?.messageIds ?? [];
379
+ while (true) {
380
+ const blob = await response.blob();
381
+ const decoded = dagCborDecode(await blob.arrayBuffer());
382
+ const {
383
+ results,
384
+ hasMore,
385
+ cursor: nextCursor,
386
+ } = MessageResultSchema.parse(decoded);
387
+ cursor = nextCursor;
388
+
389
+ const labeledMessages: LabeledMessage<Schema>[] = results.map(
390
+ (result) => {
391
+ const object =
392
+ result[LABELED_MESSAGE_MESSAGE_KEY][MESSAGE_OBJECT_KEY];
393
+ if (!validator(object)) {
394
+ throw new Error("Server returned data that does not match schema");
395
+ }
396
+ return {
397
+ ...result,
398
+ [LABELED_MESSAGE_MESSAGE_KEY]: {
399
+ ...result[LABELED_MESSAGE_MESSAGE_KEY],
400
+ [MESSAGE_OBJECT_KEY]: object,
401
+ },
402
+ };
403
+ },
404
+ );
405
+
406
+ // First cache the messages with their labels
407
+ await Promise.all(
408
+ labeledMessages.map((m: LabeledMessageBase) =>
409
+ cache.messages.set(
410
+ getMessageCacheKey(inboxUrl, m[LABELED_MESSAGE_ID_KEY]),
411
+ m,
412
+ ),
413
+ ),
414
+ );
415
+ // Then store all the messageids
416
+ messageIds = [
417
+ ...messageIds,
418
+ ...labeledMessages.map(
419
+ (m: LabeledMessageBase) => m[LABELED_MESSAGE_ID_KEY],
420
+ ),
421
+ ];
422
+ await cache.messageIds.set(messageIdsCacheKey, {
423
+ cursor,
424
+ version,
425
+ messageIds,
426
+ waitTil,
427
+ });
428
+
429
+ // Update how many we've seen
430
+ cacheNumSeen += labeledMessages.length;
431
+
432
+ // Return the values
433
+ for (const m of labeledMessages) yield m;
434
+
435
+ if (!hasMore) break;
436
+
437
+ // Otherwise get another response (after waiting for rate-limit)
438
+ await waitFor(waitTil);
439
+ const out = await this.fetchMessageBatch(
440
+ inboxUrl,
441
+ type,
442
+ undefined, // Body is never past the first time
443
+ inboxToken,
444
+ cursor,
445
+ );
446
+ response = out.response;
447
+ waitTil = out.waitTil;
448
+ }
449
+
450
+ const outputCursor: infer_<typeof CursorSchema> = {
451
+ numSeen: cacheNumSeen,
452
+ version,
453
+ messageIdsCacheKey,
454
+ objectSchema,
455
+ };
456
+
457
+ return JSON.stringify(outputCursor);
458
+ }
459
+
460
+ query<Schema extends JSONSchema>(
461
+ inboxUrl: string,
462
+ tags: Uint8Array[],
463
+ objectSchema: Schema,
464
+ inboxToken?: string | null,
465
+ ): MessageStream<Schema> {
466
+ verifyHTTPSEndpoint(inboxUrl);
467
+
468
+ const body = dagCborEncode({
469
+ tags,
470
+ schema: objectSchema,
471
+ });
472
+
473
+ const messageIdsCacheKey = getMessageIdsCacheKey(inboxUrl, "query", body);
474
+ return this.lockedMessageStreamer<Schema>(
475
+ messageIdsCacheKey,
476
+ inboxUrl,
477
+ "query",
478
+ new Uint8Array(body),
479
+ inboxToken,
480
+ objectSchema,
481
+ );
482
+ }
483
+
484
+ continueQuery(
485
+ inboxUrl: string,
486
+ cursor: string,
487
+ inboxToken?: string | null,
488
+ ): MessageStream<{}> {
489
+ verifyHTTPSEndpoint(inboxUrl);
490
+
491
+ const decodedCursor = JSON.parse(cursor);
492
+ const { messageIdsCacheKey, numSeen, objectSchema, version } =
493
+ CursorSchema.parse(decodedCursor);
494
+
495
+ return this.lockedMessageStreamer<{}>(
496
+ Promise.resolve(messageIdsCacheKey),
497
+ inboxUrl,
498
+ "query",
499
+ undefined,
500
+ inboxToken,
501
+ objectSchema,
502
+ version,
503
+ numSeen,
504
+ );
505
+ }
506
+
507
+ export(inboxUrl: string, inboxToken: string): MessageStream<{}> {
508
+ verifyHTTPSEndpoint(inboxUrl);
509
+ const messageIdsCacheKey = getMessageIdsCacheKey(inboxUrl, "export");
510
+ return this.lockedMessageStreamer<{}>(
511
+ messageIdsCacheKey,
512
+ inboxUrl,
513
+ "export",
514
+ undefined,
515
+ inboxToken,
516
+ );
517
+ }
518
+ }
519
+
520
+ const GraffitiObjectSchema = strictObject({
521
+ value: looseObject({}),
522
+ channels: array(string()),
523
+ allowed: optional(nullable(array(url()))),
524
+ url: url(),
525
+ actor: url(),
526
+ });
527
+ export const Uint8ArraySchema = custom<Uint8Array>(
528
+ (v): v is Uint8Array => v instanceof Uint8Array,
529
+ );
530
+ export const TagsSchema = array(Uint8ArraySchema);
531
+
532
+ export const MESSAGE_TAGS_KEY = "t";
533
+ export const MESSAGE_OBJECT_KEY = "o";
534
+ export const MESSAGE_METADATA_KEY = "m";
535
+ export const MessageBaseSchema = strictObject({
536
+ [MESSAGE_TAGS_KEY]: TagsSchema,
537
+ [MESSAGE_OBJECT_KEY]: GraffitiObjectSchema,
538
+ [MESSAGE_METADATA_KEY]: Uint8ArraySchema,
539
+ });
540
+ type MessageBase = infer_<typeof MessageBaseSchema>;
541
+
542
+ export const LABELED_MESSAGE_ID_KEY = "id";
543
+ export const LABELED_MESSAGE_MESSAGE_KEY = "m";
544
+ export const LABELED_MESSAGE_LABEL_KEY = "l";
545
+ export const LabeledMessageBaseSchema = strictObject({
546
+ [LABELED_MESSAGE_ID_KEY]: string(),
547
+ [LABELED_MESSAGE_MESSAGE_KEY]: MessageBaseSchema,
548
+ [LABELED_MESSAGE_LABEL_KEY]: number(),
549
+ });
550
+ type LabeledMessageBase = infer_<typeof LabeledMessageBaseSchema>;
551
+
552
+ export type Message<Schema extends JSONSchema> = MessageBase & {
553
+ [MESSAGE_OBJECT_KEY]: GraffitiObject<Schema>;
554
+ };
555
+ export type LabeledMessage<Schema extends JSONSchema> = LabeledMessageBase & {
556
+ [LABELED_MESSAGE_MESSAGE_KEY]: {
557
+ [MESSAGE_OBJECT_KEY]: GraffitiObject<Schema>;
558
+ };
559
+ };
560
+
561
+ const SendResponseSchema = strictObject({ id: string() });
562
+
563
+ const MessageResultSchema = strictObject({
564
+ results: array(LabeledMessageBaseSchema),
565
+ hasMore: boolean(),
566
+ cursor: string(),
567
+ });
568
+
569
+ const CursorSchema = strictObject({
570
+ messageIdsCacheKey: string(),
571
+ version: string(),
572
+ numSeen: int().check(nonnegative()),
573
+ objectSchema: union([looseObject({}), boolean()]),
574
+ });
575
+
576
+ export interface MessageStream<
577
+ Schema extends JSONSchema,
578
+ > extends AsyncGenerator<LabeledMessage<Schema>, string> {}
579
+
580
+ type CacheQueryValue = {
581
+ cursor: string;
582
+ version: string;
583
+ messageIds: string[];
584
+ waitTil?: number;
585
+ };
586
+
587
+ async function canUseIDB(): Promise<boolean> {
588
+ try {
589
+ if (!globalThis.indexedDB) return false;
590
+
591
+ // Small probe database
592
+ await new Promise<void>((resolve, reject) => {
593
+ const req = indexedDB.open("__idb_probe__", 1);
594
+ req.onupgradeneeded = () => req.result.createObjectStore("k");
595
+ req.onsuccess = () => {
596
+ req.result.close();
597
+ resolve();
598
+ };
599
+ req.onerror = () => reject(req.error);
600
+ });
601
+
602
+ return true;
603
+ } catch {
604
+ return false;
605
+ }
606
+ }
607
+
608
+ type Cache = {
609
+ messages: {
610
+ get(k: string): Promise<LabeledMessageBase | undefined>;
611
+ set(k: string, value: LabeledMessageBase): Promise<void>;
612
+ del(k: string): Promise<void>;
613
+ };
614
+ messageIds: {
615
+ get(k: string): Promise<CacheQueryValue | undefined>;
616
+ set(k: string, value: CacheQueryValue): Promise<void>;
617
+ del(k: string): Promise<void>;
618
+ };
619
+ };
620
+
621
+ function getMessageCacheKey(inboxUrl: string, messageId: string) {
622
+ return `${encodeURIComponent(inboxUrl)}:${encodeURIComponent(messageId)}`;
623
+ }
624
+ async function getMessageIdsCacheKey(
625
+ inboxUrl: string,
626
+ type: "query" | "export",
627
+ body?: Uint8Array,
628
+ ): Promise<string> {
629
+ const cacheIdData = dagCborEncode({
630
+ inboxUrl,
631
+ type,
632
+ body: body ?? null,
633
+ });
634
+ return crypto.subtle
635
+ .digest("SHA-256", new Uint8Array(cacheIdData))
636
+ .then((bytes) =>
637
+ Array.from(new Uint8Array(bytes))
638
+ .map((byte) => byte.toString(16).padStart(2, "0"))
639
+ .join(""),
640
+ );
641
+ }
642
+
643
+ async function resetCacheDB() {
644
+ await new Promise<void>((resolve) => {
645
+ const req = indexedDB.deleteDatabase("graffiti-inbox-cache");
646
+ req.onsuccess = () => resolve();
647
+ req.onerror = () => resolve(); // best effort
648
+ req.onblocked = () => resolve(); // best effort
649
+ });
650
+ }
651
+
652
+ async function createCache(): Promise<Cache> {
653
+ if (await canUseIDB()) {
654
+ const { openDB } = await import("idb");
655
+ const db = await openDB("graffiti-inbox-cache", 1, {
656
+ upgrade(db) {
657
+ if (!db.objectStoreNames.contains("m")) db.createObjectStore("m");
658
+ if (!db.objectStoreNames.contains("q")) db.createObjectStore("q");
659
+ },
660
+ });
661
+
662
+ return {
663
+ messages: {
664
+ get: async (k) => {
665
+ try {
666
+ return db.get("m", k);
667
+ } catch (error) {
668
+ console.error("Error getting message from cache:", error);
669
+ console.error("resetting cache...");
670
+ await resetCacheDB();
671
+ return undefined;
672
+ }
673
+ },
674
+ set: async (k, v) => {
675
+ await db.put("m", v, k);
676
+ },
677
+ del: (k) => db.delete("m", k),
678
+ },
679
+ messageIds: {
680
+ get: async (k) => {
681
+ try {
682
+ return await db.get("q", k);
683
+ } catch (error) {
684
+ console.error("Error getting message IDs from cache:", error);
685
+ console.error("resetting cache...");
686
+ await resetCacheDB();
687
+ return undefined;
688
+ }
689
+ },
690
+ set: async (k, v) => {
691
+ await db.put("q", v, k);
692
+ },
693
+ del: (k) => db.delete("q", k),
694
+ },
695
+ };
696
+ }
697
+
698
+ const m = new Map<string, LabeledMessageBase>();
699
+ const q = new Map<string, CacheQueryValue>();
700
+
701
+ return {
702
+ messages: {
703
+ get: async (k) => m.get(k),
704
+ set: async (k, v) => void m.set(k, v),
705
+ del: async (k) => void m.delete(k),
706
+ },
707
+ messageIds: {
708
+ get: async (k) => q.get(k),
709
+ set: async (k, v) => void q.set(k, v),
710
+ del: async (k) => void q.delete(k),
711
+ },
712
+ };
713
+ }
714
+
715
+ async function waitFor(waitTil?: number) {
716
+ if (waitTil !== undefined) {
717
+ const waitFor = waitTil - Date.now();
718
+ if (waitFor > 0) {
719
+ await new Promise((resolve) => setTimeout(resolve, waitFor));
720
+ }
721
+ }
722
+ }