the statusphere demo reworked into a vite/react app in a monorepo

Switch to a multiple-status model

+32 -47
+2 -2
lexicons/status.json
··· 7 7 "key": "literal:self", 8 8 "record": { 9 9 "type": "object", 10 - "required": ["status", "updatedAt"], 10 + "required": ["status", "createdAt"], 11 11 "properties": { 12 12 "status": { 13 13 "type": "string", ··· 15 15 "maxGraphemes": 1, 16 16 "maxLength": 32 17 17 }, 18 - "updatedAt": { "type": "string", "format": "datetime" } 18 + "createdAt": { "type": "string", "format": "datetime" } 19 19 } 20 20 } 21 21 }
+2 -25
package-lock.json
··· 9 9 "version": "0.0.1", 10 10 "license": "MIT", 11 11 "dependencies": { 12 + "@atproto/common": "^0.4.1", 12 13 "@atproto/identity": "^0.4.0", 13 14 "@atproto/lexicon": "0.4.1-rc.0", 14 15 "@atproto/oauth-client-node": "0.0.2-rc.2", ··· 23 24 "kysely": "^0.27.4", 24 25 "multiformats": "^9.9.0", 25 26 "pino": "^9.3.2", 26 - "pino-http": "^10.0.0", 27 27 "uhtml": "^4.5.9" 28 28 }, 29 29 "devDependencies": { ··· 142 142 "version": "0.4.1", 143 143 "resolved": "https://registry.npmjs.org/@atproto/common/-/common-0.4.1.tgz", 144 144 "integrity": "sha512-uL7kQIcBTbvkBDNfxMXL6lBH4fO2DQpHd2BryJxMtbw/4iEPKe9xBYApwECHhEIk9+zhhpTRZ15FJ3gxTXN82Q==", 145 + "license": "MIT", 145 146 "dependencies": { 146 147 "@atproto/common-web": "^0.3.0", 147 148 "@ipld/dag-cbor": "^7.0.3", ··· 1944 1945 "resolved": "https://registry.npmjs.org/gc-hook/-/gc-hook-0.3.1.tgz", 1945 1946 "integrity": "sha512-E5M+O/h2o7eZzGhzRZGex6hbB3k4NWqO0eA+OzLRLXxhdbYPajZnynPwAtphnh+cRHPwsj5Z80dqZlfI4eK55A==" 1946 1947 }, 1947 - "node_modules/get-caller-file": { 1948 - "version": "2.0.5", 1949 - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 1950 - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 1951 - "engines": { 1952 - "node": "6.* || 8.* || >= 10.*" 1953 - } 1954 - }, 1955 1948 "node_modules/get-intrinsic": { 1956 1949 "version": "1.2.4", 1957 1950 "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", ··· 2748 2741 "readable-stream": "^4.0.0", 2749 2742 "split2": "^4.0.0" 2750 2743 } 2751 - }, 2752 - "node_modules/pino-http": { 2753 - "version": "10.2.0", 2754 - "resolved": "https://registry.npmjs.org/pino-http/-/pino-http-10.2.0.tgz", 2755 - "integrity": "sha512-am03BxnV3Ckx68OkbH0iZs3indsrH78wncQ6w1w51KroIbvJZNImBKX2X1wjdY8lSyaJ0UrX/dnO2DY3cTeCRw==", 2756 - "dependencies": { 2757 - "get-caller-file": "^2.0.5", 2758 - "pino": "^9.0.0", 2759 - "pino-std-serializers": "^7.0.0", 2760 - "process-warning": "^3.0.0" 2761 - } 2762 - }, 2763 - "node_modules/pino-http/node_modules/process-warning": { 2764 - "version": "3.0.0", 2765 - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", 2766 - "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==" 2767 2744 }, 2768 2745 "node_modules/pino-pretty": { 2769 2746 "version": "11.2.2",
+1
package.json
··· 14 14 "clean": "rimraf dist coverage" 15 15 }, 16 16 "dependencies": { 17 + "@atproto/common": "^0.4.1", 17 18 "@atproto/identity": "^0.4.0", 18 19 "@atproto/lexicon": "0.4.1-rc.0", 19 20 "@atproto/oauth-client-node": "0.0.2-rc.2",
+5 -3
src/db.ts
··· 16 16 } 17 17 18 18 export type Status = { 19 + uri: string 19 20 authorDid: string 20 21 status: string 21 - updatedAt: string 22 + createdAt: string 22 23 indexedAt: string 23 24 } 24 25 ··· 50 51 async up(db: Kysely<unknown>) { 51 52 await db.schema 52 53 .createTable('status') 53 - .addColumn('authorDid', 'varchar', (col) => col.primaryKey()) 54 + .addColumn('uri', 'varchar', (col) => col.primaryKey()) 55 + .addColumn('authorDid', 'varchar', (col) => col.notNull()) 54 56 .addColumn('status', 'varchar', (col) => col.notNull()) 55 - .addColumn('updatedAt', 'varchar', (col) => col.notNull()) 57 + .addColumn('createdAt', 'varchar', (col) => col.notNull()) 56 58 .addColumn('indexedAt', 'varchar', (col) => col.notNull()) 57 59 .execute() 58 60 await db.schema
+9 -3
src/firehose/ingester.ts
··· 24 24 await this.db 25 25 .insertInto('status') 26 26 .values({ 27 + uri: evt.uri.toString(), 27 28 authorDid: evt.author, 28 29 status: record.status, 29 - updatedAt: record.updatedAt, 30 + createdAt: record.createdAt, 30 31 indexedAt: new Date().toISOString(), 31 32 }) 32 33 .onConflict((oc) => 33 - oc.column('authorDid').doUpdateSet({ 34 + oc.column('uri').doUpdateSet({ 34 35 status: record.status, 35 - updatedAt: record.updatedAt, 36 36 indexedAt: new Date().toISOString(), 37 37 }) 38 38 ) 39 39 .execute() 40 40 } 41 + } else if ( 42 + evt.event === 'delete' && 43 + evt.collection === 'com.example.status' 44 + ) { 45 + // Remove the status from our SQLite 46 + await this.db.deleteFrom('status').where({ uri: evt.uri.toString() }) 41 47 } 42 48 } 43 49 }
+2 -2
src/lexicon/lexicons.ts
··· 68 68 key: 'literal:self', 69 69 record: { 70 70 type: 'object', 71 - required: ['status', 'updatedAt'], 71 + required: ['status', 'createdAt'], 72 72 properties: { 73 73 status: { 74 74 type: 'string', ··· 76 76 maxGraphemes: 1, 77 77 maxLength: 32, 78 78 }, 79 - updatedAt: { 79 + createdAt: { 80 80 type: 'string', 81 81 format: 'datetime', 82 82 },
+1 -1
src/lexicon/types/com/example/status.ts
··· 8 8 9 9 export interface Record { 10 10 status: string 11 - updatedAt: string 11 + createdAt: string 12 12 [k: string]: unknown 13 13 } 14 14
+10 -11
src/routes.ts
··· 3 3 import type { IncomingMessage, ServerResponse } from 'node:http' 4 4 import { OAuthResolverError } from '@atproto/oauth-client-node' 5 5 import { isValidHandle } from '@atproto/syntax' 6 + import { TID } from '@atproto/common' 6 7 import express from 'express' 7 8 import { getIronSession } from 'iron-session' 8 9 import type { AppContext } from '#/index' ··· 154 155 .selectFrom('status') 155 156 .selectAll() 156 157 .where('authorDid', '=', agent.accountDid) 158 + .orderBy('indexedAt', 'desc') 157 159 .executeTakeFirst() 158 160 : undefined 159 161 ··· 204 206 } 205 207 206 208 // Construct & validate their status record 209 + const rkey = TID.nextStr() 207 210 const record = { 208 211 $type: 'com.example.status', 209 212 status: req.body?.status, 210 - updatedAt: new Date().toISOString(), 213 + createdAt: new Date().toISOString(), 211 214 } 212 215 if (!Status.validateRecord(record).success) { 213 216 return res.status(400).json({ error: 'Invalid status' }) 214 217 } 215 218 219 + let uri 216 220 try { 217 221 // Write the status record to the user's repository 218 - await agent.com.atproto.repo.putRecord({ 222 + const res = await agent.com.atproto.repo.putRecord({ 219 223 repo: agent.accountDid, 220 224 collection: 'com.example.status', 221 - rkey: 'self', 225 + rkey, 222 226 record, 223 227 validate: false, 224 228 }) 229 + uri = res.data.uri 225 230 } catch (err) { 226 231 ctx.logger.warn({ err }, 'failed to write record') 227 232 return res.status(500).json({ error: 'Failed to write record' }) ··· 235 240 await ctx.db 236 241 .insertInto('status') 237 242 .values({ 243 + uri, 238 244 authorDid: agent.accountDid, 239 245 status: record.status, 240 - updatedAt: record.updatedAt, 246 + createdAt: record.createdAt, 241 247 indexedAt: new Date().toISOString(), 242 248 }) 243 - .onConflict((oc) => 244 - oc.column('authorDid').doUpdateSet({ 245 - status: record.status, 246 - updatedAt: record.updatedAt, 247 - indexedAt: new Date().toISOString(), 248 - }) 249 - ) 250 249 .execute() 251 250 } catch (err) { 252 251 ctx.logger.warn(