Encrypted, ephemeral, private memos on atproto

feat(consumer): subscribe to jetstream

graham.systems 6a7981ea 46073a57

verified
Changed files
+74 -5
packages
consumer
+38
deno.lock
··· 13 13 "jsr:@std/internal@^1.0.10": "1.0.10", 14 14 "npm:@atcute/atproto@^3.1.9": "3.1.9", 15 15 "npm:@atcute/client@^4.0.5": "4.0.5", 16 + "npm:@atcute/jetstream@^1.1.2": "1.1.2", 16 17 "npm:@atcute/lex-cli@^2.3.1": "2.3.1", 17 18 "npm:@atcute/lexicons@^1.2.2": "1.2.2", 18 19 "npm:@atproto/lexicon@~0.5.1": "0.5.1" ··· 78 79 "@badrap/valita" 79 80 ] 80 81 }, 82 + "@atcute/jetstream@1.1.2": { 83 + "integrity": "sha512-u6p/h2xppp7LE6W/9xErAJ6frfN60s8adZuCKtfAaaBBiiYbb1CfpzN8Uc+2qtJZNorqGvuuDb5572Jmh7yHBQ==", 84 + "dependencies": [ 85 + "@atcute/lexicons", 86 + "@badrap/valita", 87 + "@mary-ext/event-iterator", 88 + "@mary-ext/simple-event-emitter", 89 + "partysocket", 90 + "type-fest", 91 + "yocto-queue" 92 + ] 93 + }, 81 94 "@atcute/lex-cli@2.3.1": { 82 95 "integrity": "sha512-HrHD91CFSFd/p0UFe3akFA1HXiboQwd5LbYiU0srKdLxGX+NLTX/EdCdhbLV6M7LsXdmxk7PB6BMcprsX4rbvg==", 83 96 "dependencies": [ ··· 128 141 "@badrap/valita@0.4.6": { 129 142 "integrity": "sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg==" 130 143 }, 144 + "@mary-ext/event-iterator@1.0.0": { 145 + "integrity": "sha512-l6gCPsWJ8aRCe/s7/oCmero70kDHgIK5m4uJvYgwEYTqVxoBOIXbKr5tnkLqUHEg6mNduB4IWvms3h70Hp9ADQ==", 146 + "dependencies": [ 147 + "yocto-queue" 148 + ] 149 + }, 150 + "@mary-ext/simple-event-emitter@1.0.0": { 151 + "integrity": "sha512-meA/zJZKIN1RVBNEYIbjufkUrW7/tRjHH60FjolpG1ixJKo76TB208qefQLNdOVDA7uIG0CGEDuhmMirtHKLAg==" 152 + }, 131 153 "@optique/core@0.6.2": { 132 154 "integrity": "sha512-HTxIHJ8xLOSZotiU6Zc5BCJv+SJ8DMYmuiQM+7tjF7RolJn/pdZNe7M78G3+DgXL9lIf82l8aGcilmgVYRQnGQ==" 133 155 }, ··· 143 165 "esm-env@1.2.2": { 144 166 "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==" 145 167 }, 168 + "event-target-polyfill@0.0.4": { 169 + "integrity": "sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ==" 170 + }, 146 171 "graphemer@1.4.0": { 147 172 "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" 148 173 }, ··· 152 177 "multiformats@9.9.0": { 153 178 "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==" 154 179 }, 180 + "partysocket@1.1.6": { 181 + "integrity": "sha512-LkEk8N9hMDDsDT0iDK0zuwUDFVrVMUXFXCeN3850Ng8wtjPqPBeJlwdeY6ROlJSEh3tPoTTasXoSBYH76y118w==", 182 + "dependencies": [ 183 + "event-target-polyfill" 184 + ] 185 + }, 155 186 "picocolors@1.1.1": { 156 187 "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" 157 188 }, 158 189 "prettier@3.6.2": { 159 190 "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", 160 191 "bin": true 192 + }, 193 + "type-fest@4.41.0": { 194 + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==" 161 195 }, 162 196 "uint8arrays@3.0.0": { 163 197 "integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==", ··· 165 199 "multiformats" 166 200 ] 167 201 }, 202 + "yocto-queue@1.2.1": { 203 + "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==" 204 + }, 168 205 "zod@3.25.76": { 169 206 "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==" 170 207 } ··· 176 213 "jsr:@puregarlic/randimal@^1.0.1", 177 214 "npm:@atcute/atproto@^3.1.9", 178 215 "npm:@atcute/client@^4.0.5", 216 + "npm:@atcute/jetstream@^1.1.2", 179 217 "npm:@atcute/lexicons@^1.2.2" 180 218 ] 181 219 },
+1
packages/consumer/deno.jsonc
··· 6 6 "imports": { 7 7 "@atcute/atproto": "npm:@atcute/atproto@^3.1.9", 8 8 "@atcute/client": "npm:@atcute/client@^4.0.5", 9 + "@atcute/jetstream": "npm:@atcute/jetstream@^1.1.2", 9 10 "@atcute/lexicons": "npm:@atcute/lexicons@^1.2.2", 10 11 "@puregarlic/randimal": "jsr:@puregarlic/randimal@^1.0.1" 11 12 }
+35 -5
packages/consumer/mod.ts
··· 1 1 import { produceRequirements } from "@cistern/shared"; 2 2 import { generateKeys } from "@cistern/crypto"; 3 3 import { generateRandomName } from "@puregarlic/randimal"; 4 - import { parse } from "@atcute/lexicons"; 4 + import { is, parse } from "@atcute/lexicons"; 5 + import { JetstreamSubscription } from "@atcute/jetstream"; 5 6 import type { Did } from "@atcute/lexicons/syntax"; 6 7 import type { Client, CredentialManager } from "@atcute/client"; 7 8 import { ··· 119 120 } 120 121 121 122 /** 122 - * Subscribes to the Jetstreams for the user's items. 123 - * @todo Add `@atcute/jetstream` dependency 124 - * @todo Return an async iterator 123 + * Subscribes to the Jetstreams for the user's items. Pass `"stop"` into `subscription.next(...)` to cancel 124 + * @todo Allow specifying Jetstream endpoint 125 125 */ 126 - async getItemSubscription() {} 126 + async *subscribeToItems(): AsyncIterator< 127 + AppCisternLexiconItem.Main, 128 + void, 129 + "stop" | undefined 130 + > { 131 + if (!this.keypair) { 132 + throw new Error("no key pair set; generate a key before subscribing"); 133 + } 134 + 135 + const subscription = new JetstreamSubscription({ 136 + url: "wss://jetstream2.us-east.bsky.network", 137 + wantedCollections: ["app.cistern.lexicon.item"], 138 + wantedDids: [this.did], 139 + }); 140 + 141 + for await (const event of subscription) { 142 + if (event.kind === "commit" && event.commit.operation === "create") { 143 + const record = event.commit.record; 144 + 145 + if (!is(AppCisternLexiconItem.mainSchema, record)) { 146 + continue; 147 + } 148 + 149 + if (record.pubkey !== this.keypair.publicKey) { 150 + continue; 151 + } 152 + 153 + if ((yield record) === "stop") return; 154 + } 155 + } 156 + } 127 157 128 158 /** 129 159 * Deletes an item from the user's PDS by record key.