refactor: logging and async services

This commit is contained in:
Ben Allfree 2022-11-20 09:14:48 -08:00
parent f5c9f349e3
commit 80f10f528d
40 changed files with 396 additions and 208 deletions

View File

@ -0,0 +1,27 @@
export type Config = {
debug: boolean
}
export type Logger = ReturnType<typeof createLogger>
export const createLogger = (config: Config) => {
const { debug } = config
const dbg = (...args: any[]) => {
if (!debug) return
console.log(`[DBG]`, ...args)
}
const warn = (...args: any[]) => {
console.log(`[WARN]`, ...args)
}
const info = (...args: any[]) => {
console.log(`[INFO]`, ...args)
}
const error = (...args: any[]) => {
console.error(`[ERROR]`, ...args)
}
return { dbg, warn, info, error }
}

View File

@ -0,0 +1,57 @@
import { ClientResponseError } from 'pocketbase'
import { Logger } from './Logger'
export type PromiseHelperConfig = {
logger: Logger
}
export type PromiseHelper = ReturnType<typeof createPromiseHelper>
export const createPromiseHelper = (config: PromiseHelperConfig) => {
const { logger } = config
const { dbg, error, warn } = logger
let inside = ''
let c = 0
const safeCatch = <TIn extends any[], TOut>(
name: string,
cb: (...args: TIn) => Promise<TOut>
) => {
return (...args: TIn) => {
const _c = c++
const uuid = `${name}:${_c}`
const pfx = `[safeCatch:${uuid}]`
// dbg(uuid, ...args)
const tid = setTimeout(() => {
warn(pfx, `timeout waiting for ${pfx}`)
}, 100)
inside = pfx
return cb(...args)
.then((res) => {
// dbg(uuid, `finished`)
inside = ''
clearTimeout(tid)
return res
})
.catch((e: any) => {
if (e instanceof ClientResponseError) {
error(pfx, `PocketBase API error ${e}`)
error(pfx, JSON.stringify(e.data, null, 2))
if (e.status === 400) {
error(
pfx,
`It looks like you don't have permission to make this request.`
)
}
} else {
error(pfx, `failed: ${e}`)
}
error(pfx, e)
throw e
})
}
}
return { safeCatch }
}

View File

@ -1,4 +1,7 @@
export * from './assert' export * from './assert'
export * from './Logger'
export * from './pocketbase-client-helpers'
export * from './PromiseHelper'
export * from './releases' export * from './releases'
export * from './schema' export * from './schema'
export * from './TimerManager' export * from './TimerManager'

View File

@ -0,0 +1,110 @@
import type pocketbaseEs from 'pocketbase'
import type { RecordSubscription } from 'pocketbase'
import type { JsonObject } from 'type-fest'
import { Logger } from '../Logger'
import { PromiseHelper } from '../PromiseHelper'
import { RecordId, RpcCommands, UserId } from '../schema'
import type { WatchHelper } from './WatchHelper'
export type RpcHelperConfig = {
client: pocketbaseEs
watchHelper: WatchHelper
promiseHelper: PromiseHelper
logger: Logger
}
export type RpcHelper = ReturnType<typeof createRpcHelper>
export enum RpcStatus {
New = 'new',
Queued = 'queued',
Running = 'running',
Starting = 'starting',
FinishedSuccess = 'finished-success',
FinishedError = 'finished-error',
}
export type RpcPayloadBase = JsonObject
export type RpcRecord_In<TRecord extends RpcRecord<any, any>> = Pick<
TRecord,
'userId' | 'payload' | 'cmd'
>
export type RpcRecord<
TPayload extends RpcPayloadBase,
TRes extends JsonObject
> = {
id: RecordId
userId: UserId
cmd: string
payload: TPayload
status: RpcStatus
message: string
result: TRes
}
export const RPC_COLLECTION = `rpc`
export const createRpcHelper = (config: RpcHelperConfig) => {
const {
client,
watchHelper: { watchById },
promiseHelper: { safeCatch },
} = config
const mkRpc = <TPayload extends JsonObject, TResult extends JsonObject>(
cmd: RpcCommands
) => {
type ConcreteRpcRecord = RpcRecord<TPayload, TResult>
return safeCatch(
cmd,
async (
payload: TPayload,
cb?: (data: RecordSubscription<ConcreteRpcRecord>) => void
) => {
const _user = client.authStore.model
if (!_user) {
throw new Error(`Expected authenticated user here.`)
}
const { id: userId } = _user
const rpcIn: RpcRecord_In<ConcreteRpcRecord> = {
cmd,
userId,
payload,
}
const rec = await client
.collection(RPC_COLLECTION)
.create<ConcreteRpcRecord>(rpcIn)
return new Promise<ConcreteRpcRecord['result']>(
async (resolve, reject) => {
const unsub = watchById<ConcreteRpcRecord>(
RPC_COLLECTION,
rec.id,
(data) => {
if (data.record.status === RpcStatus.FinishedSuccess) {
unsub.then((u) => {
u()
resolve(data.record.result)
})
return
}
if (data.record.status === RpcStatus.FinishedError) {
unsub.then((u) => {
reject(data.record.message)
u()
})
return
}
cb?.(data)
}
)
}
)
}
)
}
return { mkRpc }
}

View File

@ -0,0 +1,72 @@
import type { RecordId } from '@pockethost/common'
import type pocketbaseEs from 'pocketbase'
import type { RecordSubscription, UnsubscribeFunc } from 'pocketbase'
import { Logger } from '../Logger'
import { PromiseHelper } from '../PromiseHelper'
export type WatchHelperConfig = {
client: pocketbaseEs
promiseHelper: PromiseHelper
logger: Logger
}
export type WatchHelper = ReturnType<typeof createWatchHelper>
export const createWatchHelper = (config: WatchHelperConfig) => {
const {
client,
promiseHelper: { safeCatch },
} = config
const watchById = safeCatch(
`subscribe`,
async <TRec>(
collectionName: string,
id: RecordId,
cb: (data: RecordSubscription<TRec>) => void,
initialFetch = true
) => {
const unsub = await client
.collection(collectionName)
.subscribe<TRec>(id, cb)
if (initialFetch) {
const initial = await client.collection(collectionName).getOne<TRec>(id)
if (!initial) {
throw new Error(`Expected ${collectionName}.${id} to exist.`)
}
cb({ action: 'update', record: initial })
}
return unsub
}
)
const watchAllById = safeCatch(
`watchAllById`,
async <TRec>(
collectionName: string,
idName: keyof TRec,
idValue: RecordId,
cb: (data: RecordSubscription<TRec>) => void,
initialFetch = true
): Promise<UnsubscribeFunc> => {
const unsub = client
.collection(collectionName)
.subscribe<TRec>('*', (e) => {
// console.log(e.record.instanceId, id)
if (e.record[idName] !== idValue) return
cb(e)
})
if (initialFetch) {
const existing = await client
.collection(collectionName)
.getFullList<TRec>(100, {
filter: `${idName.toString()} = '${idValue}'`,
})
existing.forEach((record) => cb({ action: 'init', record }))
}
return unsub
}
)
return { watchById, watchAllById }
}

View File

@ -0,0 +1,2 @@
export * from './RpcHelper'
export * from './WatchHelper'

View File

@ -7,7 +7,7 @@ import {
InstanceId, InstanceId,
InstancesRecord, InstancesRecord,
} from '@pockethost/common' } from '@pockethost/common'
import { safeCatch } from '../util/safeAsync' import { safeCatch } from '../util/promiseHelper'
import { MixinContext } from './PbClient' import { MixinContext } from './PbClient'
export type BackupApi = ReturnType<typeof createBackupMixin> export type BackupApi = ReturnType<typeof createBackupMixin>

View File

@ -8,8 +8,8 @@ import {
import { reduce } from '@s-libs/micro-dash' import { reduce } from '@s-libs/micro-dash'
import Bottleneck from 'bottleneck' import Bottleneck from 'bottleneck'
import { endOfMonth, startOfMonth } from 'date-fns' import { endOfMonth, startOfMonth } from 'date-fns'
import { dbg } from '../util/dbg' import { dbg } from '../util/logger'
import { safeCatch } from '../util/safeAsync' import { safeCatch } from '../util/promiseHelper'
import { MixinContext } from './PbClient' import { MixinContext } from './PbClient'
export type InstanceApi = ReturnType<typeof createInstanceMixin> export type InstanceApi = ReturnType<typeof createInstanceMixin>

View File

@ -3,8 +3,8 @@ import {
InvocationRecord, InvocationRecord,
pocketNow, pocketNow,
} from '@pockethost/common' } from '@pockethost/common'
import { dbg } from '../util/dbg' import { dbg } from '../util/logger'
import { safeCatch } from '../util/safeAsync' import { safeCatch } from '../util/promiseHelper'
import { InstanceApi } from './InstanceMIxin' import { InstanceApi } from './InstanceMIxin'
import { MixinContext } from './PbClient' import { MixinContext } from './PbClient'

View File

@ -1,5 +1,5 @@
import { JobRecord, JobStatus } from '@pockethost/common' import { JobRecord, JobStatus } from '@pockethost/common'
import { safeCatch } from '../util/safeAsync' import { safeCatch } from '../util/promiseHelper'
import { MixinContext } from './PbClient' import { MixinContext } from './PbClient'
export enum RecordSubscriptionActions { export enum RecordSubscriptionActions {

View File

@ -6,7 +6,8 @@ import {
} from 'pocketbase' } from 'pocketbase'
import { DAEMON_PB_DATA_DIR, PUBLIC_PB_SUBDOMAIN } from '../constants' import { DAEMON_PB_DATA_DIR, PUBLIC_PB_SUBDOMAIN } from '../constants'
import { Collection_Serialized } from '../migrate/schema' import { Collection_Serialized } from '../migrate/schema'
import { safeCatch } from '../util/safeAsync' import { info } from '../util/logger'
import { safeCatch } from '../util/promiseHelper'
import { createBackupMixin } from './BackupMixin' import { createBackupMixin } from './BackupMixin'
import { createInstanceMixin } from './InstanceMIxin' import { createInstanceMixin } from './InstanceMIxin'
import { createInvocationMixin } from './InvocationMixin' import { createInvocationMixin } from './InvocationMixin'
@ -18,7 +19,7 @@ export type PocketbaseClientApi = ReturnType<typeof createPbClient>
export type MixinContext = { client: pocketbaseEs; rawDb: Knex } export type MixinContext = { client: pocketbaseEs; rawDb: Knex }
export const createPbClient = (url: string) => { export const createPbClient = (url: string) => {
console.log(`Initializing client: ${url}`) info(`Initializing client: ${url}`)
const rawDb = createRawPbClient( const rawDb = createRawPbClient(
`${DAEMON_PB_DATA_DIR}/${PUBLIC_PB_SUBDOMAIN}/pb_data/data.db` `${DAEMON_PB_DATA_DIR}/${PUBLIC_PB_SUBDOMAIN}/pb_data/data.db`
) )

View File

@ -7,8 +7,8 @@ import {
PUBLIC_PB_SUBDOMAIN, PUBLIC_PB_SUBDOMAIN,
} from '../constants' } from '../constants'
import { backupInstance } from '../util/backupInstance' import { backupInstance } from '../util/backupInstance'
import { error } from '../util/dbg' import { dbg, error, info } from '../util/logger'
import { safeCatch } from '../util/safeAsync' import { safeCatch } from '../util/promiseHelper'
import { pexec } from './pexec' import { pexec } from './pexec'
import { schema } from './schema' import { schema } from './schema'
import { withInstance } from './withInstance' import { withInstance } from './withInstance'
@ -20,11 +20,11 @@ safeCatch(`root`, async () => {
PUBLIC_PB_SUBDOMAIN, PUBLIC_PB_SUBDOMAIN,
`${+new Date()}`, `${+new Date()}`,
async (progress) => { async (progress) => {
console.log(progress) dbg(progress)
} }
) )
console.log(`Upgrading`) info(`Upgrading`)
await pexec(`${PB_BIN} upgrade --dir=pb_data`) await pexec(`${PB_BIN} upgrade --dir=pb_data`)
await withInstance(async (client) => { await withInstance(async (client) => {

View File

@ -1,13 +1,15 @@
import { exec } from 'child_process' import { exec } from 'child_process'
import { safeCatch } from '../util/safeAsync' import { dbg, error } from '../util/logger'
import { safeCatch } from '../util/promiseHelper'
export const pexec = safeCatch(`pexec`, (cmd: string) => { export const pexec = safeCatch(`pexec`, (cmd: string) => {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
console.log(cmd) dbg(cmd)
exec(cmd, (err, stdout, stderr) => { exec(cmd, (err, stdout, stderr) => {
console.log(stdout) dbg(stdout)
console.error(stderr)
if (err) { if (err) {
error(`${err}`)
error(stderr)
reject(err) reject(err)
return return
} }

View File

@ -9,7 +9,8 @@ import {
} from '../constants' } from '../constants'
import { createPbClient, PocketbaseClientApi } from '../db/PbClient' import { createPbClient, PocketbaseClientApi } from '../db/PbClient'
import { mkInternalUrl } from '../util/internal' import { mkInternalUrl } from '../util/internal'
import { safeCatch } from '../util/safeAsync' import { error, info } from '../util/logger'
import { safeCatch } from '../util/promiseHelper'
import { spawnInstance } from '../util/spawnInstance' import { spawnInstance } from '../util/spawnInstance'
import { tryFetch } from '../util/tryFetch' import { tryFetch } from '../util/tryFetch'
@ -31,18 +32,16 @@ export const withInstance = safeCatch(
await client.adminAuthViaEmail(DAEMON_PB_USERNAME, DAEMON_PB_PASSWORD) await client.adminAuthViaEmail(DAEMON_PB_USERNAME, DAEMON_PB_PASSWORD)
await cb(client) await cb(client)
} catch (e) { } catch (e) {
console.error( error(
`***WARNING*** CANNOT AUTHENTICATE TO ${PUBLIC_PB_PROTOCOL}://${PUBLIC_PB_SUBDOMAIN}.${PUBLIC_PB_DOMAIN}/_/` `***WARNING*** CANNOT AUTHENTICATE TO ${PUBLIC_PB_PROTOCOL}://${PUBLIC_PB_SUBDOMAIN}.${PUBLIC_PB_DOMAIN}/_/`
) )
console.error( error(`***WARNING*** LOG IN MANUALLY, ADJUST .env, AND RESTART DOCKER`)
`***WARNING*** LOG IN MANUALLY, ADJUST .env, AND RESTART DOCKER`
)
} finally { } finally {
console.log(`Exiting process`) info(`Exiting process`)
mainProcess.kill() mainProcess.kill()
} }
} catch (e) { } catch (e) {
console.error(`${e}`) error(`${e}`)
} }
} }
) )

View File

@ -13,6 +13,7 @@ import { createInstanceService } from './services/InstanceService'
import { createJobService } from './services/JobService' import { createJobService } from './services/JobService'
import { createProxyService } from './services/ProxyService' import { createProxyService } from './services/ProxyService'
import { mkInternalUrl } from './util/internal' import { mkInternalUrl } from './util/internal'
import { dbg, error, info } from './util/logger'
import { spawnInstance } from './util/spawnInstance' import { spawnInstance } from './util/spawnInstance'
// npm install eventsource --save // npm install eventsource --save
global.EventSource = require('eventsource') global.EventSource = require('eventsource')
@ -36,14 +37,12 @@ global.EventSource = require('eventsource')
const instanceService = await createInstanceService(client) const instanceService = await createInstanceService(client)
try { try {
await client.adminAuthViaEmail(DAEMON_PB_USERNAME, DAEMON_PB_PASSWORD) await client.adminAuthViaEmail(DAEMON_PB_USERNAME, DAEMON_PB_PASSWORD)
console.log(`Logged in`) dbg(`Logged in`)
} catch (e) { } catch (e) {
console.error( error(
`***WARNING*** CANNOT AUTHENTICATE TO ${PUBLIC_PB_PROTOCOL}://${PUBLIC_PB_SUBDOMAIN}.${PUBLIC_PB_DOMAIN}/_/` `***WARNING*** CANNOT AUTHENTICATE TO ${PUBLIC_PB_PROTOCOL}://${PUBLIC_PB_SUBDOMAIN}.${PUBLIC_PB_DOMAIN}/_/`
) )
console.error( error(`***WARNING*** LOG IN MANUALLY, ADJUST .env, AND RESTART DOCKER`)
`***WARNING*** LOG IN MANUALLY, ADJUST .env, AND RESTART DOCKER`
)
} }
const proxyService = await createProxyService(instanceService) const proxyService = await createProxyService(instanceService)
@ -51,7 +50,7 @@ global.EventSource = require('eventsource')
const backupService = await createBackupService(client, jobService) const backupService = await createBackupService(client, jobService)
process.once('SIGUSR2', async () => { process.once('SIGUSR2', async () => {
console.log(`SIGUSR2 detected`) info(`SIGUSR2 detected`)
proxyService.shutdown() proxyService.shutdown()
instanceService.shutdown() instanceService.shutdown()
jobService.shutdown() jobService.shutdown()

View File

@ -10,7 +10,7 @@ import {
import Bottleneck from 'bottleneck' import Bottleneck from 'bottleneck'
import { PocketbaseClientApi } from '../db/PbClient' import { PocketbaseClientApi } from '../db/PbClient'
import { backupInstance } from '../util/backupInstance' import { backupInstance } from '../util/backupInstance'
import { dbg } from '../util/dbg' import { dbg } from '../util/logger'
import { JobServiceApi } from './JobService' import { JobServiceApi } from './JobService'
export const createBackupService = async ( export const createBackupService = async (
@ -66,7 +66,7 @@ export const createBackupService = async (
tm.repeat(async () => { tm.repeat(async () => {
const backupRec = await client.getNextBackupJob() const backupRec = await client.getNextBackupJob()
if (!backupRec) { if (!backupRec) {
dbg(`No backups requested`) // dbg(`No backups requested`)
return true return true
} }
const instance = await client.getInstance(backupRec.instanceId) const instance = await client.getInstance(backupRec.instanceId)

View File

@ -16,10 +16,10 @@ import {
PUBLIC_APP_PROTOCOL, PUBLIC_APP_PROTOCOL,
} from '../constants' } from '../constants'
import { PocketbaseClientApi } from '../db/PbClient' import { PocketbaseClientApi } from '../db/PbClient'
import { dbg } from '../util/dbg'
import { mkInternalUrl } from '../util/internal' import { mkInternalUrl } from '../util/internal'
import { dbg, error, warn } from '../util/logger'
import { now } from '../util/now' import { now } from '../util/now'
import { safeCatch } from '../util/safeAsync' import { safeCatch } from '../util/promiseHelper'
import { PocketbaseProcess, spawnInstance } from '../util/spawnInstance' import { PocketbaseProcess, spawnInstance } from '../util/spawnInstance'
type InstanceApi = { type InstanceApi = {
@ -68,7 +68,7 @@ export const createInstanceService = async (client: PocketbaseClientApi) => {
port: DAEMON_PB_PORT_BASE, port: DAEMON_PB_PORT_BASE,
exclude, exclude,
}).catch((e) => { }).catch((e) => {
console.error(`Failed to get port for ${subdomain}`) error(`Failed to get port for ${subdomain}`)
throw e throw e
}) })
dbg(`Found port for ${subdomain}: ${newPort}`) dbg(`Found port for ${subdomain}: ${newPort}`)
@ -81,7 +81,7 @@ export const createInstanceService = async (client: PocketbaseClientApi) => {
port: newPort, port: newPort,
bin: binFor(instance.platform, instance.version), bin: binFor(instance.platform, instance.version),
onUnexpectedStop: (code) => { onUnexpectedStop: (code) => {
console.warn(`${subdomain} exited unexpectedly with ${code}`) warn(`${subdomain} exited unexpectedly with ${code}`)
api.shutdown() api.shutdown()
}, },
}) })

View File

@ -11,7 +11,7 @@ import { default as knexFactory } from 'knex'
import pocketbaseEs from 'pocketbase' import pocketbaseEs from 'pocketbase'
import { AsyncReturnType } from 'type-fest' import { AsyncReturnType } from 'type-fest'
import { PocketbaseClientApi } from '../db/PbClient' import { PocketbaseClientApi } from '../db/PbClient'
import { error } from '../util/dbg' import { dbg, error } from '../util/logger'
export type JobServiceApi = AsyncReturnType<typeof createJobService> export type JobServiceApi = AsyncReturnType<typeof createJobService>
@ -46,7 +46,7 @@ export const createJobService = async (client: PocketbaseClientApi) => {
if (!handler) { if (!handler) {
throw new Error(`Job handler ${cmd} is not registered`) throw new Error(`Job handler ${cmd} is not registered`)
} }
console.log(`Running job ${job.id}`, job) dbg(`Running job ${job.id}`, job)
await client.setJobStatus(job, JobStatus.Running) await client.setJobStatus(job, JobStatus.Running)
await handler(job) await handler(job)
await client.setJobStatus(job, JobStatus.FinishedSuccess) await client.setJobStatus(job, JobStatus.FinishedSuccess)

View File

@ -7,8 +7,8 @@ import {
PUBLIC_APP_PROTOCOL, PUBLIC_APP_PROTOCOL,
PUBLIC_PB_SUBDOMAIN, PUBLIC_PB_SUBDOMAIN,
} from '../constants' } from '../constants'
import { dbg, info } from '../util/dbg'
import { mkInternalUrl } from '../util/internal' import { mkInternalUrl } from '../util/internal'
import { dbg, error, info } from '../util/logger'
import { InstanceServiceApi } from './InstanceService' import { InstanceServiceApi } from './InstanceService'
export type ProxyServiceApi = AsyncReturnType<typeof createProxyService> export type ProxyServiceApi = AsyncReturnType<typeof createProxyService>
@ -22,7 +22,7 @@ export const createProxyService = async (
dbg(`Incoming request ${req.headers.host}/${req.url}`) dbg(`Incoming request ${req.headers.host}/${req.url}`)
const die = (msg: string) => { const die = (msg: string) => {
console.error(`ERROR: ${msg}`) error(msg)
res.writeHead(403, { res.writeHead(403, {
'Content-Type': `text/plain`, 'Content-Type': `text/plain`,
}) })

View File

@ -6,9 +6,9 @@ import { Database } from 'sqlite3'
import tmp from 'tmp' import tmp from 'tmp'
import { DAEMON_PB_DATA_DIR } from '../constants' import { DAEMON_PB_DATA_DIR } from '../constants'
import { pexec } from '../migrate/pexec' import { pexec } from '../migrate/pexec'
import { dbg, error } from './dbg'
import { ensureDirExists } from './ensureDirExists' import { ensureDirExists } from './ensureDirExists'
import { safeCatch } from './safeAsync' import { dbg, error } from './logger'
import { safeCatch } from './promiseHelper'
export type BackupProgress = { export type BackupProgress = {
current: number current: number
@ -75,7 +75,7 @@ export const backupInstance = safeCatch(
unsafeCleanup: true, unsafeCleanup: true,
}) })
const backupTmpTargetRoot = resolve(tmpObj.name) const backupTmpTargetRoot = resolve(tmpObj.name)
console.log({ dbg({
instanceId, instanceId,
dataRoot, dataRoot,
backupTgzRoot, backupTgzRoot,
@ -109,7 +109,7 @@ export const backupInstance = safeCatch(
error(`${e}`) error(`${e}`)
throw e throw e
} finally { } finally {
console.log(`Removing again ${backupTmpTargetRoot}`) dbg(`Removing again ${backupTmpTargetRoot}`)
tmpObj.removeCallback() tmpObj.removeCallback()
chdir(_cwd) chdir(_cwd)
} }

View File

@ -1,14 +0,0 @@
import { DEBUG } from '../constants'
export const dbg = (...args: any[]) => {
if (!DEBUG) return
console.log(...args)
}
export const info = (...args: any[]) => {
console.log(...args)
}
export const error = (...args: any[]) => {
console.error(...args)
}

View File

@ -1,5 +1,5 @@
import { mkdirSync } from 'fs' import { mkdirSync } from 'fs'
import { dbg } from './dbg' import { dbg } from './logger'
export const ensureDirExists = (path: string) => { export const ensureDirExists = (path: string) => {
try { try {

View File

@ -0,0 +1,5 @@
import { createLogger } from '@pockethost/common'
import { DEBUG } from '../constants'
export const logger = createLogger({ debug: DEBUG })
export const { dbg, info, warn, error } = logger

View File

@ -0,0 +1,5 @@
import { createPromiseHelper } from '@pockethost/common'
import { logger } from './logger'
export const promiseHelper = createPromiseHelper({ logger })
export const { safeCatch } = promiseHelper

View File

@ -1,66 +0,0 @@
import Bottleneck from 'bottleneck'
import { ClientResponseError } from 'pocketbase'
import { dbg, error } from './dbg'
const limiter = new Bottleneck({ maxConcurrent: 1 })
let inside = ''
export const serialAsync = <TIn extends any[], TOut>(
name: string,
cb: (...args: TIn) => Promise<TOut>
) => {
const _cb = safeCatch(name, cb)
return (...args: TIn) => {
return limiter.schedule(() => {
if (inside) {
throw new Error(
`Already in async function ${inside}, can't execute ${name}`
)
}
return _cb(...args).finally(() => {
inside = ''
})
})
}
}
let c = 0
export const safeCatch = <TIn extends any[], TOut>(
name: string,
cb: (...args: TIn) => Promise<TOut>
) => {
return (...args: TIn) => {
const _c = c++
const uuid = `${name}:${_c}`
dbg(uuid, ...args)
const tid = setTimeout(() => {
dbg(uuid, `WARNING: timeout waiting for ${uuid}`)
}, 100)
inside = uuid
return cb(...args)
.then((res) => {
dbg(uuid, `finished`)
inside = ''
clearTimeout(tid)
return res
})
.catch((e: any) => {
if (e instanceof ClientResponseError) {
error(uuid, `PocketBase API error ${e}`)
error(uuid, JSON.stringify(e.data, null, 2))
if (e.status === 400) {
error(
uuid,
`It looks like you don't have permission to make this request.`
)
}
} else {
error(uuid, `failed: ${e}`)
}
error(uuid, e)
throw e
})
}
}

View File

@ -2,9 +2,9 @@ import { spawn } from 'child_process'
import { existsSync } from 'fs' import { existsSync } from 'fs'
import { AsyncReturnType } from 'type-fest' import { AsyncReturnType } from 'type-fest'
import { DAEMON_PB_BIN_DIR, DAEMON_PB_DATA_DIR } from '../constants' import { DAEMON_PB_BIN_DIR, DAEMON_PB_DATA_DIR } from '../constants'
import { dbg } from './dbg'
import { mkInternalAddress, mkInternalUrl } from './internal' import { mkInternalAddress, mkInternalUrl } from './internal'
import { safeCatch } from './safeAsync' import { dbg, error } from './logger'
import { safeCatch } from './promiseHelper'
import { tryFetch } from './tryFetch' import { tryFetch } from './tryFetch'
export type PocketbaseProcess = AsyncReturnType<typeof spawnInstance> export type PocketbaseProcess = AsyncReturnType<typeof spawnInstance>
@ -40,7 +40,7 @@ export const spawnInstance = safeCatch(`spawnInstance`, async (cfg: Config) => {
}) })
ls.stderr.on('data', (data) => { ls.stderr.on('data', (data) => {
console.error(`${subdomain} stderr: ${data}`) error(`${subdomain} stderr: ${data}`)
}) })
ls.on('close', (code) => { ls.on('close', (code) => {

View File

@ -1,5 +1,5 @@
import { dbg, error } from './dbg' import { dbg, error } from './logger'
import { safeCatch } from './safeAsync' import { safeCatch } from './promiseHelper'
export const tryFetch = safeCatch( export const tryFetch = safeCatch(
`tryFetch`, `tryFetch`,

View File

@ -1,12 +1,13 @@
<script lang="ts"> <script lang="ts">
import CopyButton from '$components/CopyButton.svelte'
import { dbg } from '$util/logger'
import { Highlight } from 'svelte-highlight' import { Highlight } from 'svelte-highlight'
import { typescript } from 'svelte-highlight/languages' import { typescript } from 'svelte-highlight/languages'
import 'svelte-highlight/styles/github.css' import 'svelte-highlight/styles/github.css'
import CopyButton from '$components/CopyButton.svelte'
export let code: string export let code: string
const handleCopy = () => { const handleCopy = () => {
console.log('copied') dbg('copied')
} }
</script> </script>

View File

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { client } from '$src/pocketbase' import { client } from '$src/pocketbase'
import { warn } from '$util/logger'
import publicRoutes from '$util/public-routes.json' import publicRoutes from '$util/public-routes.json'
import { getRouter } from '$util/utilities' import { getRouter } from '$util/utilities'
import { onMount } from 'svelte' import { onMount } from 'svelte'
@ -11,7 +12,7 @@
const { pathname } = router const { pathname } = router
if (!publicRoutes.includes(pathname)) { if (!publicRoutes.includes(pathname)) {
console.warn(`${pathname} is a private route`) warn(`${pathname} is a private route`)
window.location.href = '/' window.location.href = '/'
} }
}) })

View File

@ -1,6 +1,7 @@
import { createGenericSyncEvent } from '$util/events' import { createGenericSyncEvent } from '$util/events'
import { import {
assertExists, assertExists,
createWatchHelper,
JobCommands, JobCommands,
JobStatus, JobStatus,
type BackupRecord, type BackupRecord,
@ -13,6 +14,8 @@ import {
type InstancesRecord_New, type InstancesRecord_New,
type JobRecord, type JobRecord,
type JobRecord_In, type JobRecord_In,
type Logger,
type PromiseHelper,
type UserRecord type UserRecord
} from '@pockethost/common' } from '@pockethost/common'
import { keys, map } from '@s-libs/micro-dash' import { keys, map } from '@s-libs/micro-dash'
@ -24,7 +27,6 @@ import PocketBase, {
type RecordSubscription, type RecordSubscription,
type UnsubscribeFunc type UnsubscribeFunc
} from 'pocketbase' } from 'pocketbase'
import { safeCatch } from '../util/safeCatch'
export type AuthChangeHandler = (user: BaseAuthStore) => void export type AuthChangeHandler = (user: BaseAuthStore) => void
@ -35,9 +37,18 @@ export type AuthStoreProps = {
isValid: boolean isValid: boolean
} }
export type PocketbaseClientConfig = {
url: string
logger: Logger
promiseHelper: PromiseHelper
}
export type PocketbaseClient = ReturnType<typeof createPocketbaseClient> export type PocketbaseClient = ReturnType<typeof createPocketbaseClient>
export const createPocketbaseClient = (url: string) => { export const createPocketbaseClient = (config: PocketbaseClientConfig) => {
const { url, logger, promiseHelper } = config
const { dbg, error } = logger
const { safeCatch } = promiseHelper
const client = new PocketBase(url) const client = new PocketBase(url)
const { authStore } = client const { authStore } = client
@ -57,7 +68,7 @@ export const createPocketbaseClient = (url: string) => {
passwordConfirm: password passwordConfirm: password
}) })
.then(() => { .then(() => {
// console.log(`Sending verification email to ${email}`) // dbg(`Sending verification email to ${email}`)
return client.collection('users').requestVerification(email) return client.collection('users').requestVerification(email)
}) })
) )
@ -99,6 +110,9 @@ export const createPocketbaseClient = (url: string) => {
client.collection('users').authRefresh() client.collection('users').authRefresh()
) )
const watchHelper = createWatchHelper({ client, promiseHelper, logger })
const { watchById, watchAllById } = watchHelper
const createInstance = safeCatch( const createInstance = safeCatch(
`createInstance`, `createInstance`,
(payload: InstancesRecord_New): Promise<InstancesRecord> => { (payload: InstancesRecord_New): Promise<InstancesRecord> => {
@ -115,41 +129,15 @@ export const createPocketbaseClient = (url: string) => {
const watchInstanceById = async ( const watchInstanceById = async (
id: InstanceId, id: InstanceId,
cb: (data: RecordSubscription<InstancesRecord>) => void cb: (data: RecordSubscription<InstancesRecord>) => void
): Promise<UnsubscribeFunc> => { ): Promise<UnsubscribeFunc> => watchById('instances', id, cb)
getInstanceById(id).then((record) => {
// console.log(`Got instnace`, record)
assertExists(record, `Expected instance ${id} here`)
cb({ action: 'init', record })
})
return client.collection('instances').subscribe<InstancesRecord>(id, cb)
}
const watchBackupsByInstanceId = async ( const watchBackupsByInstanceId = async (
id: InstanceId, id: InstanceId,
cb: (data: RecordSubscription<BackupRecord>) => void cb: (data: RecordSubscription<BackupRecord>) => void
): Promise<UnsubscribeFunc> => { ): Promise<UnsubscribeFunc> => watchAllById('backups', 'instanceId', id, cb)
const unsub = client.collection('backups').subscribe<BackupRecord>('*', (e) => {
// console.log(e.record.instanceId, id)
if (e.record.instanceId !== id) return
cb(e)
})
const existingBackups = await client
.collection('backups')
.getFullList<BackupRecord>(100, { filter: `instanceId = '${id}'` })
existingBackups.forEach((record) => cb({ action: 'init', record }))
return unsub
}
const getAllInstancesById = safeCatch(`getAllInstancesById`, async () => const getAllInstancesById = safeCatch(`getAllInstancesById`, async () =>
( (await client.collection('instances').getFullList()).reduce((c, v) => {
await client
.collection('instances')
.getFullList()
.catch((e) => {
// console.error(`getAllInstancesById failed with ${e}`)
throw e
})
).reduce((c, v) => {
c[v.id] = v c[v.id] = v
return c return c
}, {} as Record) }, {} as Record)
@ -169,7 +157,7 @@ export const createPocketbaseClient = (url: string) => {
const getAuthStoreProps = (): AuthStoreProps => { const getAuthStoreProps = (): AuthStoreProps => {
const { token, model, isValid } = client.authStore as AuthStoreProps const { token, model, isValid } = client.authStore as AuthStoreProps
// console.log(`current authStore`, { token, model, isValid }) // dbg(`current authStore`, { token, model, isValid })
if (model instanceof Admin) throw new Error(`Admin models not supported`) if (model instanceof Admin) throw new Error(`Admin models not supported`)
if (model && !model.email) throw new Error(`Expected model to be a user here`) if (model && !model.email) throw new Error(`Expected model to be a user here`)
return { return {
@ -202,7 +190,7 @@ export const createPocketbaseClient = (url: string) => {
*/ */
refreshAuthToken() refreshAuthToken()
.catch((e) => { .catch((e) => {
// console.error(`Clearing auth store: ${e}`) dbg(`Clearing auth store: ${e}`)
client.authStore.clear() client.authStore.clear()
}) })
.finally(() => { .finally(() => {
@ -218,7 +206,7 @@ export const createPocketbaseClient = (url: string) => {
* watch on the user record and update auth accordingly. * watch on the user record and update auth accordingly.
*/ */
const unsub = onAuthChange((authStore) => { const unsub = onAuthChange((authStore) => {
// console.log(`onAuthChange`, { ...authStore }) // dbg(`onAuthChange`, { ...authStore })
const { model } = authStore const { model } = authStore
if (!model) return if (!model) return
if (model instanceof Admin) return if (model instanceof Admin) return
@ -230,9 +218,9 @@ export const createPocketbaseClient = (url: string) => {
setTimeout(_check, 1000) setTimeout(_check, 1000)
// FIXME - THIS DOES NOT WORK, WE HAVE TO POLL INSTEAD. FIX IN V0.8 // FIXME - THIS DOES NOT WORK, WE HAVE TO POLL INSTEAD. FIX IN V0.8
// console.log(`watching _users`) // dbg(`watching _users`)
// unsub = subscribe<User>(`users/${model.id}`, (user) => { // unsub = subscribe<User>(`users/${model.id}`, (user) => {
// console.log(`realtime _users change`, { ...user }) // dbg(`realtime _users change`, { ...user })
// fireAuthChange({ ...authStore, model: user }) // fireAuthChange({ ...authStore, model: user })
// }) // })
}) })

View File

@ -1,5 +1,6 @@
import { browser } from '$app/environment' import { browser, dev } from '$app/environment'
import { PUBLIC_PB_DOMAIN, PUBLIC_PB_SUBDOMAIN } from '$src/env' import { PUBLIC_PB_DOMAIN, PUBLIC_PB_SUBDOMAIN } from '$src/env'
import { createLogger, createPromiseHelper } from '@pockethost/common'
import { createPocketbaseClient, type PocketbaseClient } from './PocketbaseClient' import { createPocketbaseClient, type PocketbaseClient } from './PocketbaseClient'
export const client = (() => { export const client = (() => {
@ -7,9 +8,11 @@ export const client = (() => {
return () => { return () => {
if (!browser) throw new Error(`PocketBase client not supported in SSR`) if (!browser) throw new Error(`PocketBase client not supported in SSR`)
if (clientInstance) return clientInstance if (clientInstance) return clientInstance
console.log(`Initializing pocketbase client`) const logger = createLogger({ debug: dev })
logger.info(`Initializing pocketbase client`)
const url = `https://${PUBLIC_PB_SUBDOMAIN}.${PUBLIC_PB_DOMAIN}` const url = `https://${PUBLIC_PB_SUBDOMAIN}.${PUBLIC_PB_DOMAIN}`
clientInstance = createPocketbaseClient(url) const promiseHelper = createPromiseHelper({ logger })
clientInstance = createPocketbaseClient({ url, logger, promiseHelper })
return clientInstance return clientInstance
} }
})() })()

View File

@ -3,7 +3,8 @@
import AuthStateGuard from '$components/helpers/AuthStateGuard.svelte' import AuthStateGuard from '$components/helpers/AuthStateGuard.svelte'
import { client } from '$src/pocketbase' import { client } from '$src/pocketbase'
import { createCleanupManagerSync } from '$util/CleanupManager' import { createCleanupManagerSync } from '$util/CleanupManager'
import { assertExists } from '@pockethost/common/src/assert' import { dbg } from '$util/logger'
import { assertExists } from '@pockethost/common'
import { onDestroy, onMount } from 'svelte' import { onDestroy, onMount } from 'svelte'
import { instance } from './store' import { instance } from './store'
@ -14,7 +15,7 @@
onMount(async () => { onMount(async () => {
const { watchInstanceById } = client() const { watchInstanceById } = client()
watchInstanceById(instanceId, (r) => { watchInstanceById(instanceId, (r) => {
console.log(`Handling instance update`, r) dbg(`Handling instance update`, r)
const { action, record } = r const { action, record } = r
assertExists(record, `Expected instance here`) assertExists(record, `Expected instance here`)
instance.set(record) instance.set(record)

View File

@ -23,7 +23,7 @@
onMount(async () => { onMount(async () => {
const { watchBackupsByInstanceId } = client() const { watchBackupsByInstanceId } = client()
watchBackupsByInstanceId(instance.id, (r) => { watchBackupsByInstanceId(instance.id, (r) => {
// console.log(`Handling backup update`, r) // dbg(`Handling backup update`, r)
const { action, record } = r const { action, record } = r
const _backups = reduce( const _backups = reduce(
$backups, $backups,
@ -43,7 +43,7 @@
return Date.parse(e.created) return Date.parse(e.created)
}).reverse() }).reverse()
) )
// console.log(record.id) // dbg(record.id)
}).then(cm.add) }).then(cm.add)
}) })
onDestroy(cm.cleanupAll) onDestroy(cm.cleanupAll)

View File

@ -6,6 +6,7 @@
import { PUBLIC_PB_DOMAIN } from '$src/env' import { PUBLIC_PB_DOMAIN } from '$src/env'
import { client } from '$src/pocketbase' import { client } from '$src/pocketbase'
import { createCleanupManagerSync } from '$util/CleanupManager' import { createCleanupManagerSync } from '$util/CleanupManager'
import { error } from '$util/logger'
import { humanVersion, type InstanceRecordById, type InstancesRecord } from '@pockethost/common' import { humanVersion, type InstanceRecordById, type InstancesRecord } from '@pockethost/common'
import { forEach, values } from '@s-libs/micro-dash' import { forEach, values } from '@s-libs/micro-dash'
import { onDestroy, onMount } from 'svelte' import { onDestroy, onMount } from 'svelte'
@ -51,7 +52,7 @@
}) })
}) })
.catch((e) => { .catch((e) => {
console.error(`Failed to fetch instances`) error(`Failed to fetch instances`)
}) })
}) })

View File

@ -1,12 +1,13 @@
import { goto } from '$app/navigation' import { goto } from '$app/navigation'
import { client } from '$src/pocketbase' import { client } from '$src/pocketbase'
import { InstanceStatus, LATEST_PLATFORM, USE_LATEST_VERSION } from '@pockethost/common' import { InstanceStatus, LATEST_PLATFORM, USE_LATEST_VERSION } from '@pockethost/common'
import { dbg, error, warn } from './logger'
export type FormErrorHandler = (value: string) => void export type FormErrorHandler = (value: string) => void
export const handleFormError = (error: any, setError?: FormErrorHandler) => { export const handleFormError = (error: any, setError?: FormErrorHandler) => {
const { parseError } = client() const { parseError } = client()
console.error(`Form error: ${error}`, { error }) error(`Form error: ${error}`, { error })
if (setError) { if (setError) {
const message = parseError(error)[0] const message = parseError(error)[0]
@ -177,10 +178,10 @@ export const handleInstanceGeneratorWidget = async (
// populated the form with their existing login. Try using it. // populated the form with their existing login. Try using it.
await handleLogin(email, password, undefined, false) await handleLogin(email, password, undefined, false)
.then(() => { .then(() => {
console.log(`Account ${email} already exists. Logged in.`) dbg(`Account ${email} already exists. Logged in.`)
}) })
.catch((e) => { .catch((e) => {
console.warn(`Login failed, attempting account creation.`) warn(`Login failed, attempting account creation.`)
// This means login has failed. // This means login has failed.
// Either their credentials were incorrect, or the account // Either their credentials were incorrect, or the account
// did not exist, or there is a system issue. // did not exist, or there is a system issue.
@ -188,15 +189,15 @@ export const handleInstanceGeneratorWidget = async (
// is already in use. // is already in use.
return handleRegistration(email, password) return handleRegistration(email, password)
.then(() => { .then(() => {
console.log(`Account created, proceeding to log in.`) dbg(`Account created, proceeding to log in.`)
// This means registration succeeded. That's good. // This means registration succeeded. That's good.
// Log in using the new credentials // Log in using the new credentials
return handleLogin(email, password, undefined, false) return handleLogin(email, password, undefined, false)
.then(() => { .then(() => {
console.log(`Logged in after account creation`) dbg(`Logged in after account creation`)
}) })
.catch((e) => { .catch((e) => {
console.error(`Panic, auth system down`) error(`Panic, auth system down`)
// This should never happen. // This should never happen.
// If registration succeeds, login should always succeed. // If registration succeeds, login should always succeed.
// If a login fails at this point, the system is broken. // If a login fails at this point, the system is broken.
@ -206,7 +207,7 @@ export const handleInstanceGeneratorWidget = async (
}) })
}) })
.catch((e) => { .catch((e) => {
console.warn(`User input error`) warn(`User input error`)
// This is just for clarity // This is just for clarity
// If registration fails at this point, it means both // If registration fails at this point, it means both
// login and account creation failed. // login and account creation failed.
@ -218,16 +219,16 @@ export const handleInstanceGeneratorWidget = async (
}) })
}) })
console.log(`User before instance creation is `, user()) dbg(`User before instance creation is `, user())
// We can only get here if we are successfully logged in using the credentials // We can only get here if we are successfully logged in using the credentials
// provided by the user. // provided by the user.
// Instance creation could still fail if the name is taken // Instance creation could still fail if the name is taken
await handleCreateNewInstance(instanceName) await handleCreateNewInstance(instanceName)
.then(() => { .then(() => {
console.log(`Creation of ${instanceName} succeeded`) dbg(`Creation of ${instanceName} succeeded`)
}) })
.catch((e) => { .catch((e) => {
console.warn(`Creation of ${instanceName} failed`) warn(`Creation of ${instanceName} failed`)
// The instance creation could most likely fail if the name is taken. // The instance creation could most likely fail if the name is taken.
// In any case, bail out to show errors. // In any case, bail out to show errors.
if (e.data?.data?.subdomain?.code === 'validation_not_unique') { if (e.data?.data?.subdomain?.code === 'validation_not_unique') {
@ -240,7 +241,7 @@ export const handleInstanceGeneratorWidget = async (
throw new Error(`Instance creation: ${messages[0]}`) throw new Error(`Instance creation: ${messages[0]}`)
}) })
} catch (error: any) { } catch (error: any) {
console.error(`Caught widget error`, { error }) error(`Caught widget error`, { error })
handleFormError(error, setError) handleFormError(error, setError)
} }
} }

View File

@ -0,0 +1,5 @@
import { createLogger } from '@pockethost/common'
import { PUBLIC_DEBUG } from '../env'
export const logger = createLogger({ debug: PUBLIC_DEBUG })
export const { dbg, info, warn, error } = logger

View File

@ -1,16 +0,0 @@
import { dev } from '$app/environment'
export const safeCatch = <TIn extends any[], TOut>(
name: string,
cb: (...args: TIn) => Promise<TOut>
) => {
return (...args: TIn) => {
if (dev) {
console.log(`${name}`)
}
return cb(...args).catch((e: any) => {
console.error(`${name} failed: ${e}`)
throw e
})
}
}

View File

@ -2,6 +2,7 @@ import { browser } from '$app/environment'
import { client } from '$src/pocketbase' import { client } from '$src/pocketbase'
import type { AuthStoreProps } from '$src/pocketbase/PocketbaseClient' import type { AuthStoreProps } from '$src/pocketbase/PocketbaseClient'
import { writable } from 'svelte/store' import { writable } from 'svelte/store'
import { dbg } from './logger'
export const authStoreState = writable<AuthStoreProps>({ isValid: false, model: null, token: '' }) export const authStoreState = writable<AuthStoreProps>({ isValid: false, model: null, token: '' })
export const isUserLoggedIn = writable(false) export const isUserLoggedIn = writable(false)
@ -15,14 +16,14 @@ if (browser) {
* Listen for auth change events. When we get at least one, the auth state is initialized. * Listen for auth change events. When we get at least one, the auth state is initialized.
*/ */
onAuthChange((authStoreProps) => { onAuthChange((authStoreProps) => {
console.log(`onAuthChange in store`, { ...authStoreProps }) dbg(`onAuthChange in store`, { ...authStoreProps })
authStoreState.set(authStoreProps) authStoreState.set(authStoreProps)
isAuthStateInitialized.set(true) isAuthStateInitialized.set(true)
}) })
// Update derived stores when authStore changes // Update derived stores when authStore changes
authStoreState.subscribe((authStoreProps) => { authStoreState.subscribe((authStoreProps) => {
console.log(`subscriber change`, authStoreProps) dbg(`subscriber change`, authStoreProps)
isUserLoggedIn.set(authStoreProps.isValid) isUserLoggedIn.set(authStoreProps.isValid)
isUserVerified.set(!!authStoreProps.model?.verified) isUserVerified.set(!!authStoreProps.model?.verified)
}) })

View File

@ -139,7 +139,7 @@ open https://pockethost.io
- [x] fix: incorrect instance information displaying on dashboard details - [x] fix: incorrect instance information displaying on dashboard details
- [x] fix: more helpful error message when backup fails for nonexistent instance - [x] fix: more helpful error message when backup fails for nonexistent instance
- [x] chore: move version number to base package.json - [x] chore: move version number to base package.json
- [ ] chore: refactor logging and async helpers - [x] refctor: logging and async helpers
- [x] chore: restore autocancellation - [x] chore: restore autocancellation
- [x] chore: rebuild with go 1.19.3 and include in bin name - [x] chore: rebuild with go 1.19.3 and include in bin name
- [ ] fix: Disallow backups if data dir doesn't exist - [ ] fix: Disallow backups if data dir doesn't exist