updated max line length

+3 -5
eslint.config.js
··· 6 import react from 'eslint-plugin-react' 7 import reactHooks from 'eslint-plugin-react-hooks' 8 import tsdoc from 'eslint-plugin-tsdoc' 9 import globals from 'globals' 10 import path from 'node:path' 11 import tseslint from 'typescript-eslint' 12 13 const gitignore = path.resolve(import.meta.dirname, '.gitignore') 14 15 - export default tseslint.config( 16 includeIgnoreFile(gitignore, '.gitignore'), 17 18 // all files by default get shared globals ··· 54 '@typescript-eslint/restrict-template-expressions': 'off', 55 56 // I need to be able to do `while(true)`, come on yall... 57 - '@typescript-eslint/no-unnecessary-condition': [ 58 - 'warn', 59 - {allowConstantLoopConditions: 'always'}, 60 - ], 61 62 // this breaks when I want to use a type parameter to allow type checking inline arguments 63 // it's "unnecessary type parameters" or an `as` declaration, which I don't like
··· 6 import react from 'eslint-plugin-react' 7 import reactHooks from 'eslint-plugin-react-hooks' 8 import tsdoc from 'eslint-plugin-tsdoc' 9 + import {defineConfig} from 'eslint/config' 10 import globals from 'globals' 11 import path from 'node:path' 12 import tseslint from 'typescript-eslint' 13 14 const gitignore = path.resolve(import.meta.dirname, '.gitignore') 15 16 + export default defineConfig( 17 includeIgnoreFile(gitignore, '.gitignore'), 18 19 // all files by default get shared globals ··· 55 '@typescript-eslint/restrict-template-expressions': 'off', 56 57 // I need to be able to do `while(true)`, come on yall... 58 + '@typescript-eslint/no-unnecessary-condition': ['warn', {allowConstantLoopConditions: 'always'}], 59 60 // this breaks when I want to use a type parameter to allow type checking inline arguments 61 // it's "unnecessary type parameters" or an `as` declaration, which I don't like
+1 -1
prettier.config.js
··· 3 * @type {import("prettier").Config} 4 */ 5 export default { 6 - printWidth: 100, 7 tabWidth: 2, 8 semi: false, 9 singleQuote: true,
··· 3 * @type {import("prettier").Config} 4 */ 5 export default { 6 + printWidth: 120, 7 tabWidth: 2, 8 semi: false, 9 singleQuote: true,
+1 -3
src/client/components/feed-import-nytimes.tsx
··· 113 return ( 114 <div className="feed-import-nytimes"> 115 <button type="button" onClick={importAll} disabled={importing$.value}> 116 - {importing$.value 117 - ? `Importing... (${imported$.value}/${NYTIMES_FEEDS.length})` 118 - : 'Import All NY Times Feeds'} 119 </button> 120 121 {errors$.value.length > 0 && (
··· 113 return ( 114 <div className="feed-import-nytimes"> 115 <button type="button" onClick={importAll} disabled={importing$.value}> 116 + {importing$.value ? `Importing... (${imported$.value}/${NYTIMES_FEEDS.length})` : 'Import All NY Times Feeds'} 117 </button> 118 119 {errors$.value.length > 0 && (
+1 -3
src/client/components/feed-import-podcasts.tsx
··· 61 return ( 62 <div className="feed-import-podcasts"> 63 <button type="button" onClick={importAll} disabled={importing$.value}> 64 - {importing$.value 65 - ? `Importing... (${imported$.value}/${PODCAST_FEEDS.length})` 66 - : 'Import Podcast Feeds'} 67 </button> 68 69 {errors$.value.length > 0 && (
··· 61 return ( 62 <div className="feed-import-podcasts"> 63 <button type="button" onClick={importAll} disabled={importing$.value}> 64 + {importing$.value ? `Importing... (${imported$.value}/${PODCAST_FEEDS.length})` : 'Import Podcast Feeds'} 65 </button> 66 67 {errors$.value.length > 0 && (
+1 -3
src/client/components/feed-import-tech.tsx
··· 60 return ( 61 <div className="feed-import-tech"> 62 <button type="button" onClick={importAll} disabled={importing$.value}> 63 - {importing$.value 64 - ? `Importing... (${imported$.value}/${TECH_FEEDS.length})` 65 - : 'Import Tech Feeds'} 66 </button> 67 68 {errors$.value.length > 0 && (
··· 60 return ( 61 <div className="feed-import-tech"> 62 <button type="button" onClick={importAll} disabled={importing$.value}> 63 + {importing$.value ? `Importing... (${imported$.value}/${TECH_FEEDS.length})` : 'Import Tech Feeds'} 64 </button> 65 66 {errors$.value.length > 0 && (
+1 -5
src/client/page-app.tsx
··· 22 const wsurl = `${wsproto}://${wshost}/stream` 23 24 return ( 25 - <SkypodProvider 26 - identityFallback={identityFallback} 27 - connectionFallback={connectionFallback} 28 - websocketUrl={wsurl} 29 - > 30 <RealmConnectionManager /> 31 <PeerList /> 32 <FeedImportNYTimes />
··· 22 const wsurl = `${wsproto}://${wshost}/stream` 23 24 return ( 25 + <SkypodProvider identityFallback={identityFallback} connectionFallback={connectionFallback} websocketUrl={wsurl}> 26 <RealmConnectionManager /> 27 <PeerList /> 28 <FeedImportNYTimes />
+2 -6
src/client/root/context-database.tsx
··· 13 useDbSignal: <T>(querier: DbQuerier<T>) => ReadonlySignal<T | undefined> 14 } 15 16 - export const DatabaseProvider: preact.FunctionComponent<{children: preact.ComponentChildren}> = ( 17 - props, 18 - ) => { 19 const db = useMemo(() => new Database(), []) 20 21 function useDbSignal<T>(querier: DbQuerier<T>): ReadonlySignal<T | undefined> { ··· 32 return signal 33 } 34 35 - return ( 36 - <DatabaseContext.Provider value={{db, useDbSignal}}>{props.children}</DatabaseContext.Provider> 37 - ) 38 } 39 40 export function useDatabase() {
··· 13 useDbSignal: <T>(querier: DbQuerier<T>) => ReadonlySignal<T | undefined> 14 } 15 16 + export const DatabaseProvider: preact.FunctionComponent<{children: preact.ComponentChildren}> = (props) => { 17 const db = useMemo(() => new Database(), []) 18 19 function useDbSignal<T>(querier: DbQuerier<T>): ReadonlySignal<T | undefined> { ··· 30 return signal 31 } 32 33 + return <DatabaseContext.Provider value={{db, useDbSignal}}>{props.children}</DatabaseContext.Provider> 34 } 35 36 export function useDatabase() {
+3 -14
src/client/skypod/action-dispatch/context.tsx
··· 8 9 import {Action, ActionMap, ActionOpts} from '#skypod/actions' 10 11 - export type MiddlewareFn = ( 12 - this: undefined, 13 - action: Action, 14 - ) => void | Action[] | Promise<void | Action[]> 15 16 export type MiddlewareRouterFn<K extends keyof ActionMap> = ( 17 this: undefined, ··· 89 } 90 }, 91 92 - action: <N extends keyof ActionMap>( 93 - msg: N, 94 - dat: ActionMap[N]['dat'], 95 - opt?: ActionOpts, 96 - ): ActionMap[N] => { 97 const clk = identity.clock.now() 98 return {typ: 'act', clk, msg, dat, opt} as ActionMap[N] 99 }, ··· 135 }, 136 }) 137 138 - return ( 139 - <ActionDispatchContext.Provider value={context.current}> 140 - {props.children} 141 - </ActionDispatchContext.Provider> 142 - ) 143 } 144 145 export function useActionDispatch() {
··· 8 9 import {Action, ActionMap, ActionOpts} from '#skypod/actions' 10 11 + export type MiddlewareFn = (this: undefined, action: Action) => void | Action[] | Promise<void | Action[]> 12 13 export type MiddlewareRouterFn<K extends keyof ActionMap> = ( 14 this: undefined, ··· 86 } 87 }, 88 89 + action: <N extends keyof ActionMap>(msg: N, dat: ActionMap[N]['dat'], opt?: ActionOpts): ActionMap[N] => { 90 const clk = identity.clock.now() 91 return {typ: 'act', clk, msg, dat, opt} as ActionMap[N] 92 }, ··· 128 }, 129 }) 130 131 + return <ActionDispatchContext.Provider value={context.current}>{props.children}</ActionDispatchContext.Provider> 132 } 133 134 export function useActionDispatch() {
+2 -14
src/client/skypod/feed-processor/worker.ts
··· 47 48 async processPending() { 49 const pendingFeeds = this.#db.feeds.where('lastRefresh.status').equals('pending') 50 - return await this.#db.withLock( 51 - 'feeds', 52 - pendingFeeds, 53 - this.#clock, 54 - this.#owner, 55 - this.#processLocked, 56 - ) 57 } 58 59 async processUrls(urls: string[]) { 60 const requestedFeeds = this.#db.feeds.where('url').anyOf(urls) 61 - return await this.#db.withLock( 62 - 'feeds', 63 - requestedFeeds, 64 - this.#clock, 65 - this.#owner, 66 - this.#processLocked, 67 - ) 68 } 69 70 #poll = () => {
··· 47 48 async processPending() { 49 const pendingFeeds = this.#db.feeds.where('lastRefresh.status').equals('pending') 50 + return await this.#db.withLock('feeds', pendingFeeds, this.#clock, this.#owner, this.#processLocked) 51 } 52 53 async processUrls(urls: string[]) { 54 const requestedFeeds = this.#db.feeds.where('url').anyOf(urls) 55 + return await this.#db.withLock('feeds', requestedFeeds, this.#clock, this.#owner, this.#processLocked) 56 } 57 58 #poll = () => {
+1 -4
src/cmd/register-ident.ts
··· 15 pubkey: await jwkExport.parseAsync(keypair.publicKey), 16 } 17 18 - const jwt = await generateSignableJwt(payload) 19 - .setIssuedAt() 20 - .setExpirationTime('1m') 21 - .sign(keypair.privateKey) 22 23 console.log('Generated Preauth JWT:') 24 console.log(jwt)
··· 15 pubkey: await jwkExport.parseAsync(keypair.publicKey), 16 } 17 18 + const jwt = await generateSignableJwt(payload).setIssuedAt().setExpirationTime('1m').sign(keypair.privateKey) 19 20 console.log('Generated Preauth JWT:') 21 console.log(jwt)
+5 -12
src/common/crypto/cipher.ts
··· 43 const nonce = encoder.encode(`${nonceStr}-nonce-cryptosystem`) 44 const derivedsalt = new Uint8Array([...salt, ...nonce]) 45 46 - return await crypto.subtle.deriveKey( 47 - {...deriveAlgo, salt: derivedsalt, iterations}, 48 - derivedkey, 49 - encrAlgo, 50 - false, 51 - ['encrypt', 'decrypt'], 52 - ) 53 } 54 55 /** ··· 96 async encrypt(data: string | Uint8Array): Promise<string> { 97 const iv = crypto.getRandomValues(new Uint8Array(12)) 98 const encoded = asUint8Array(data) 99 - const encrypted = await crypto.subtle.encrypt( 100 - {...encrAlgo, iv}, 101 - this.#cryptokey, 102 - encoded as BufferSource, 103 - ) 104 105 // output = [iv + encrypted] which gives us the auth tag 106
··· 43 const nonce = encoder.encode(`${nonceStr}-nonce-cryptosystem`) 44 const derivedsalt = new Uint8Array([...salt, ...nonce]) 45 46 + return await crypto.subtle.deriveKey({...deriveAlgo, salt: derivedsalt, iterations}, derivedkey, encrAlgo, false, [ 47 + 'encrypt', 48 + 'decrypt', 49 + ]) 50 } 51 52 /** ··· 93 async encrypt(data: string | Uint8Array): Promise<string> { 94 const iv = crypto.getRandomValues(new Uint8Array(12)) 95 const encoded = asUint8Array(data) 96 + const encrypted = await crypto.subtle.encrypt({...encrAlgo, iv}, this.#cryptokey, encoded as BufferSource) 97 98 // output = [iv + encrypted] which gives us the auth tag 99
+13 -15
src/common/crypto/jwts.ts
··· 20 * schema describing a decoded JWT. 21 * **important** - this does no claims validation, only decoding from string to JWT! 22 */ 23 - export const jwtSchema: z.ZodType<JWTToken, string> = z 24 - .jwt({abort: true}) 25 - .transform((token, ctx) => { 26 - try { 27 - const claims = jose.decodeJwt(token) 28 - return {claims, token} 29 - } catch (e) { 30 - ctx.issues.push({ 31 - code: 'custom', 32 - message: `error while decoding token: ${e}`, 33 - input: token, 34 - }) 35 36 - return z.NEVER 37 - } 38 - }) 39 40 /** 41 * schema describing a verified payload in a JWT.
··· 20 * schema describing a decoded JWT. 21 * **important** - this does no claims validation, only decoding from string to JWT! 22 */ 23 + export const jwtSchema: z.ZodType<JWTToken, string> = z.jwt({abort: true}).transform((token, ctx) => { 24 + try { 25 + const claims = jose.decodeJwt(token) 26 + return {claims, token} 27 + } catch (e) { 28 + ctx.issues.push({ 29 + code: 'custom', 30 + message: `error while decoding token: ${e}`, 31 + input: token, 32 + }) 33 34 + return z.NEVER 35 + } 36 + }) 37 38 /** 39 * schema describing a verified payload in a JWT.
+2 -9
src/common/socket.ts
··· 66 } 67 68 /** exactly take socket, but will additionally apply a json decoding */ 69 - export async function takeSocketJson<T>( 70 - ws: WebSocket, 71 - schema: z.ZodType<T>, 72 - signal?: AbortSignal, 73 - ): Promise<T> { 74 const data = await takeSocket(ws, signal) 75 return parseJson.pipe(schema).parseAsync(data) 76 } ··· 191 * exactly stream socket, but will additionally apply a json decoding 192 * messages not validating will end the stream with an error 193 */ 194 - export async function* streamSocketJson( 195 - ws: WebSocket, 196 - config?: Partial<ConfigProps>, 197 - ): AsyncGenerator { 198 for await (const message of streamSocket(ws, config)) { 199 yield parseJson.parseAsync(message) 200 }
··· 66 } 67 68 /** exactly take socket, but will additionally apply a json decoding */ 69 + export async function takeSocketJson<T>(ws: WebSocket, schema: z.ZodType<T>, signal?: AbortSignal): Promise<T> { 70 const data = await takeSocket(ws, signal) 71 return parseJson.pipe(schema).parseAsync(data) 72 } ··· 187 * exactly stream socket, but will additionally apply a json decoding 188 * messages not validating will end the stream with an error 189 */ 190 + export async function* streamSocketJson(ws: WebSocket, config?: Partial<ConfigProps>): AsyncGenerator { 191 for await (const message of streamSocket(ws, config)) { 192 yield parseJson.parseAsync(message) 193 }
+1 -4
src/realm/client/components/connection-manager.tsx
··· 81 </p> 82 <p> 83 Exchange an invite: 84 - <textarea 85 - value={invitation.value} 86 - onInput={(e) => (invitation.value = e.currentTarget.value)} 87 - /> 88 <button type="button" onClick={exchange}> 89 Exchange 90 </button>
··· 81 </p> 82 <p> 83 Exchange an invite: 84 + <textarea value={invitation.value} onInput={(e) => (invitation.value = e.currentTarget.value)} /> 85 <button type="button" onClick={exchange}> 86 Exchange 87 </button>
+1 -3
src/realm/client/context-identity.tsx
··· 45 }, [store, value$, error$]) 46 47 return value$.value ? ( 48 - <RealmIdentityContext.Provider value={value$.value}> 49 - {props.children} 50 - </RealmIdentityContext.Provider> 51 ) : ( 52 props.fallback({loading: !error$.value, error: error$.value}) 53 )
··· 45 }, [store, value$, error$]) 46 47 return value$.value ? ( 48 + <RealmIdentityContext.Provider value={value$.value}>{props.children}</RealmIdentityContext.Provider> 49 ) : ( 50 props.fallback({loading: !error$.value, error: error$.value}) 51 )
+1 -4
src/realm/client/service-connection-peer.ts
··· 34 super({ 35 initiator, 36 config: { 37 - iceServers: [ 38 - {urls: 'stun:stun.l.google.com:19302'}, 39 - {urls: 'stun:stun1.l.google.com:19302'}, 40 - ], 41 }, 42 }) 43
··· 34 super({ 35 initiator, 36 config: { 37 + iceServers: [{urls: 'stun:stun.l.google.com:19302'}, {urls: 'stun:stun1.l.google.com:19302'}], 38 }, 39 }) 40
+1 -3
src/realm/client/service-connection.ts
··· 333 334 switch (parse.data.msg) { 335 case 'realm.rtc.signal': { 336 - const jwt = await jwtPayload(protocol.realmRtcSignalPayloadSchema).parseAsync( 337 - parse.data.dat.signed, 338 - ) 339 340 // backwards from our perspective 341 const remoteid = parse.data.dat.localid
··· 333 334 switch (parse.data.msg) { 335 case 'realm.rtc.signal': { 336 + const jwt = await jwtPayload(protocol.realmRtcSignalPayloadSchema).parseAsync(parse.data.dat.signed) 337 338 // backwards from our perspective 339 const remoteid = parse.data.dat.localid
+1 -5
src/realm/protocol/index.ts
··· 7 export * from './brands' 8 export * from './messages' 9 10 - export function makeError( 11 - error: ProtocolError, 12 - detail: string, 13 - seq?: number, 14 - ): z.infer<typeof errorMessageSchema> { 15 return { 16 typ: 'err', 17 msg: error.message,
··· 7 export * from './brands' 8 export * from './messages' 9 10 + export function makeError(error: ProtocolError, detail: string, seq?: number): z.infer<typeof errorMessageSchema> { 11 return { 12 typ: 'err', 13 msg: error.message,
+3 -14
src/realm/protocol/messages.ts
··· 4 import {IdentBrand} from './brands' 5 import {deviceCapsSchema, deviceInfoSchema} from './device' 6 import {LogicalClock} from './logical-clock' 7 - import { 8 - makeEmptyRequestSchema, 9 - makeEventSchema, 10 - makeRequestSchema, 11 - makeResponseSchema, 12 - } from './schema' 13 14 export const serverPeerIdSchema = z.literal('server') 15 export type ServerPeerId = z.infer<typeof serverPeerIdSchema> ··· 18 19 /// preauth 20 21 - export const preauthRegisterReqSchema = makeRequestSchema( 22 - 'preauth.register', 23 - z.object({pubkey: jwkSchema}), 24 - ) 25 26 export const preauthAuthnReqSchema = makeEmptyRequestSchema('preauth.authn') 27 ··· 70 }), 71 ) 72 73 - export const realmRtcPongResponseSchema = makeResponseSchema( 74 - 'realm.rtc.pong', 75 - z.object({peerClocks: peerClocksSchema}), 76 - ) 77 78 export const realmRtcAnnounceRequestSchema = makeRequestSchema( 79 'realm.rtc.announce',
··· 4 import {IdentBrand} from './brands' 5 import {deviceCapsSchema, deviceInfoSchema} from './device' 6 import {LogicalClock} from './logical-clock' 7 + import {makeEmptyRequestSchema, makeEventSchema, makeRequestSchema, makeResponseSchema} from './schema' 8 9 export const serverPeerIdSchema = z.literal('server') 10 export type ServerPeerId = z.infer<typeof serverPeerIdSchema> ··· 13 14 /// preauth 15 16 + export const preauthRegisterReqSchema = makeRequestSchema('preauth.register', z.object({pubkey: jwkSchema})) 17 18 export const preauthAuthnReqSchema = makeEmptyRequestSchema('preauth.authn') 19 ··· 62 }), 63 ) 64 65 + export const realmRtcPongResponseSchema = makeResponseSchema('realm.rtc.pong', z.object({peerClocks: peerClocksSchema})) 66 67 export const realmRtcAnnounceRequestSchema = makeRequestSchema( 68 'realm.rtc.announce',
+4 -18
src/realm/server/handler-preauth.ts
··· 5 import {jwtPayload, jwtSchema, JWTToken, verifyJwtToken} from '#common/crypto/jwts' 6 import {normalizeError, ProtocolError} from '#common/errors' 7 import {takeSocket} from '#common/socket' 8 - import { 9 - IdentBrand, 10 - IdentID, 11 - preauthReqSchema, 12 - PreauthResponse, 13 - RealmBrand, 14 - RealmID, 15 - } from '#realm/protocol/index' 16 17 import * as realms from './state' 18 ··· 21 * - if the realm does not exist (by realm id), we create a new one, and add the identity (success). 22 * - if the realm /does/ exist, we verify the message is a signed JWT our already registered pubkey. 23 */ 24 - export async function preauthHandler( 25 - ws: WebSocket, 26 - signal?: AbortSignal, 27 - ): Promise<realms.AuthenticatedIdentity> { 28 const timeout = timeoutSignal(3000) 29 const combinedSignal = combineSignals(signal, timeout.signal) 30 ··· 75 const realm = realmMap.require(realmid) 76 77 if (!invitation.claims.jti) throw new Error('invitation requires nonce!') 78 - if (!(await realms.validateNonce(realmid, invitation.claims.jti))) 79 - throw new Error('invitation already used!') 80 81 const inviterid = IdentBrand.parse(invitation.claims.iss) 82 const inviterkey = realm.storage.identities.require(inviterid) ··· 107 } 108 } 109 110 - async function preauthResponse( 111 - auth: realms.AuthenticatedIdentity, 112 - seq?: number, 113 - ): Promise<PreauthResponse> { 114 const peers: Array<IdentID | 'server'> = Array.from(auth.realm.sockets.keys()) 115 peers.unshift('server') 116
··· 5 import {jwtPayload, jwtSchema, JWTToken, verifyJwtToken} from '#common/crypto/jwts' 6 import {normalizeError, ProtocolError} from '#common/errors' 7 import {takeSocket} from '#common/socket' 8 + import {IdentBrand, IdentID, preauthReqSchema, PreauthResponse, RealmBrand, RealmID} from '#realm/protocol/index' 9 10 import * as realms from './state' 11 ··· 14 * - if the realm does not exist (by realm id), we create a new one, and add the identity (success). 15 * - if the realm /does/ exist, we verify the message is a signed JWT our already registered pubkey. 16 */ 17 + export async function preauthHandler(ws: WebSocket, signal?: AbortSignal): Promise<realms.AuthenticatedIdentity> { 18 const timeout = timeoutSignal(3000) 19 const combinedSignal = combineSignals(signal, timeout.signal) 20 ··· 65 const realm = realmMap.require(realmid) 66 67 if (!invitation.claims.jti) throw new Error('invitation requires nonce!') 68 + if (!(await realms.validateNonce(realmid, invitation.claims.jti))) throw new Error('invitation already used!') 69 70 const inviterid = IdentBrand.parse(invitation.claims.iss) 71 const inviterkey = realm.storage.identities.require(inviterid) ··· 96 } 97 } 98 99 + async function preauthResponse(auth: realms.AuthenticatedIdentity, seq?: number): Promise<PreauthResponse> { 100 const peers: Array<IdentID | 'server'> = Array.from(auth.realm.sockets.keys()) 101 peers.unshift('server') 102
+4 -16
src/realm/server/handler-realm.ts
··· 24 * ance we've retrieved authentication details, we go into the main realm loop. 25 * read messages as they come in and dispatch actions. 26 */ 27 - export async function realmHandler( 28 - ws: WebSocket, 29 - auth: realm.AuthenticatedIdentity, 30 - signal?: AbortSignal, 31 - ) { 32 realmBroadcast(auth, await buildRtcPeerJoined(auth)) 33 34 try { ··· 77 } 78 } 79 80 - async function buildRtcPeerJoined( 81 - auth: realm.AuthenticatedIdentity, 82 - ): Promise<protocol.RealmRtcPeerJoinedEvent> { 83 return { 84 typ: 'evt', 85 msg: 'realm.rtc.peer-joined', ··· 112 recipients: protocol.IdentID[] | boolean = false, 113 ) { 114 const echo = recipients === true || Array.isArray(recipients) 115 - const recips = Array.isArray(recipients) 116 - ? recipients 117 - : Array.from(auth.realm.storage.identities.keys()) 118 const json = JSON.stringify(payload) 119 120 for (const recip of recips) { ··· 129 } 130 } 131 132 - async function socketPeerPing( 133 - ws: WebSocket, 134 - auth: realm.AuthenticatedIdentity, 135 - ping: protocol.RealmRtcPingRequest, 136 - ) { 137 console.debug('ping from', auth.identid, ping.dat) 138 139 const peerClocks = await auth.realm.storage.buildSyncState()
··· 24 * ance we've retrieved authentication details, we go into the main realm loop. 25 * read messages as they come in and dispatch actions. 26 */ 27 + export async function realmHandler(ws: WebSocket, auth: realm.AuthenticatedIdentity, signal?: AbortSignal) { 28 realmBroadcast(auth, await buildRtcPeerJoined(auth)) 29 30 try { ··· 73 } 74 } 75 76 + async function buildRtcPeerJoined(auth: realm.AuthenticatedIdentity): Promise<protocol.RealmRtcPeerJoinedEvent> { 77 return { 78 typ: 'evt', 79 msg: 'realm.rtc.peer-joined', ··· 106 recipients: protocol.IdentID[] | boolean = false, 107 ) { 108 const echo = recipients === true || Array.isArray(recipients) 109 + const recips = Array.isArray(recipients) ? recipients : Array.from(auth.realm.storage.identities.keys()) 110 const json = JSON.stringify(payload) 111 112 for (const recip of recips) { ··· 121 } 122 } 123 124 + async function socketPeerPing(ws: WebSocket, auth: realm.AuthenticatedIdentity, ping: protocol.RealmRtcPingRequest) { 125 console.debug('ping from', auth.identid, ping.dat) 126 127 const peerClocks = await auth.realm.storage.buildSyncState()
+2 -11
src/realm/server/state-storage.ts
··· 88 return new RealmStorage(realmid, identities, db, metapath) 89 } 90 91 - static async ensure( 92 - realmid: RealmID, 93 - registrantid: IdentID, 94 - registrantkey: CryptoKey, 95 - ): Promise<RealmStorage> { 96 try { 97 return await this.open(realmid) 98 } catch { ··· 118 #db: Level 119 #metadataPath: string 120 121 - private constructor( 122 - realmid: RealmID, 123 - identities: StrictMap<IdentID, CryptoKey>, 124 - db: Level, 125 - metadataPath: string, 126 - ) { 127 this.realmid = realmid 128 this.identities = identities 129
··· 88 return new RealmStorage(realmid, identities, db, metapath) 89 } 90 91 + static async ensure(realmid: RealmID, registrantid: IdentID, registrantkey: CryptoKey): Promise<RealmStorage> { 92 try { 93 return await this.open(realmid) 94 } catch { ··· 114 #db: Level 115 #metadataPath: string 116 117 + private constructor(realmid: RealmID, identities: StrictMap<IdentID, CryptoKey>, db: Level, metadataPath: string) { 118 this.realmid = realmid 119 this.identities = identities 120
+1 -5
src/skypod/actions.ts
··· 18 export type ActionOpts = z.infer<typeof actionOptionsSchema> 19 export type ActionMap = {[K in Action as K['msg']]: K} 20 21 - export function makeAction<N extends Action['msg']>( 22 - msg: N, 23 - clk: LCTimestamp, 24 - dat: ActionMap[N]['dat'], 25 - ): ActionMap[N] { 26 return {typ: 'act', clk, msg, dat} as ActionMap[N] 27 }
··· 18 export type ActionOpts = z.infer<typeof actionOptionsSchema> 19 export type ActionMap = {[K in Action as K['msg']]: K} 20 21 + export function makeAction<N extends Action['msg']>(msg: N, clk: LCTimestamp, dat: ActionMap[N]['dat']): ActionMap[N] { 22 return {typ: 'act', clk, msg, dat} as ActionMap[N] 23 }