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 './Logger'
export * from './pocketbase-client-helpers'
export * from './PromiseHelper'
export * from './releases'
export * from './schema'
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,
InstancesRecord,
} from '@pockethost/common'
import { safeCatch } from '../util/safeAsync'
import { safeCatch } from '../util/promiseHelper'
import { MixinContext } from './PbClient'
export type BackupApi = ReturnType<typeof createBackupMixin>

View File

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

View File

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

View File

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

View File

@ -6,7 +6,8 @@ import {
} from 'pocketbase'
import { DAEMON_PB_DATA_DIR, PUBLIC_PB_SUBDOMAIN } from '../constants'
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 { createInstanceMixin } from './InstanceMIxin'
import { createInvocationMixin } from './InvocationMixin'
@ -18,7 +19,7 @@ export type PocketbaseClientApi = ReturnType<typeof createPbClient>
export type MixinContext = { client: pocketbaseEs; rawDb: Knex }
export const createPbClient = (url: string) => {
console.log(`Initializing client: ${url}`)
info(`Initializing client: ${url}`)
const rawDb = createRawPbClient(
`${DAEMON_PB_DATA_DIR}/${PUBLIC_PB_SUBDOMAIN}/pb_data/data.db`
)

View File

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

View File

@ -1,13 +1,15 @@
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) => {
return new Promise<void>((resolve, reject) => {
console.log(cmd)
dbg(cmd)
exec(cmd, (err, stdout, stderr) => {
console.log(stdout)
console.error(stderr)
dbg(stdout)
if (err) {
error(`${err}`)
error(stderr)
reject(err)
return
}

View File

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

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@ import { default as knexFactory } from 'knex'
import pocketbaseEs from 'pocketbase'
import { AsyncReturnType } from 'type-fest'
import { PocketbaseClientApi } from '../db/PbClient'
import { error } from '../util/dbg'
import { dbg, error } from '../util/logger'
export type JobServiceApi = AsyncReturnType<typeof createJobService>
@ -46,7 +46,7 @@ export const createJobService = async (client: PocketbaseClientApi) => {
if (!handler) {
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 handler(job)
await client.setJobStatus(job, JobStatus.FinishedSuccess)

View File

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

View File

@ -6,9 +6,9 @@ import { Database } from 'sqlite3'
import tmp from 'tmp'
import { DAEMON_PB_DATA_DIR } from '../constants'
import { pexec } from '../migrate/pexec'
import { dbg, error } from './dbg'
import { ensureDirExists } from './ensureDirExists'
import { safeCatch } from './safeAsync'
import { dbg, error } from './logger'
import { safeCatch } from './promiseHelper'
export type BackupProgress = {
current: number
@ -75,7 +75,7 @@ export const backupInstance = safeCatch(
unsafeCleanup: true,
})
const backupTmpTargetRoot = resolve(tmpObj.name)
console.log({
dbg({
instanceId,
dataRoot,
backupTgzRoot,
@ -109,7 +109,7 @@ export const backupInstance = safeCatch(
error(`${e}`)
throw e
} finally {
console.log(`Removing again ${backupTmpTargetRoot}`)
dbg(`Removing again ${backupTmpTargetRoot}`)
tmpObj.removeCallback()
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 { dbg } from './dbg'
import { dbg } from './logger'
export const ensureDirExists = (path: string) => {
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 { AsyncReturnType } from 'type-fest'
import { DAEMON_PB_BIN_DIR, DAEMON_PB_DATA_DIR } from '../constants'
import { dbg } from './dbg'
import { mkInternalAddress, mkInternalUrl } from './internal'
import { safeCatch } from './safeAsync'
import { dbg, error } from './logger'
import { safeCatch } from './promiseHelper'
import { tryFetch } from './tryFetch'
export type PocketbaseProcess = AsyncReturnType<typeof spawnInstance>
@ -40,7 +40,7 @@ export const spawnInstance = safeCatch(`spawnInstance`, async (cfg: Config) => {
})
ls.stderr.on('data', (data) => {
console.error(`${subdomain} stderr: ${data}`)
error(`${subdomain} stderr: ${data}`)
})
ls.on('close', (code) => {

View File

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

View File

@ -39,4 +39,4 @@
"sass": "^1.54.9",
"svelte-highlight": "^6.2.1"
}
}
}

View File

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

View File

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

View File

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

View File

@ -3,7 +3,8 @@
import AuthStateGuard from '$components/helpers/AuthStateGuard.svelte'
import { client } from '$src/pocketbase'
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 { instance } from './store'
@ -14,7 +15,7 @@
onMount(async () => {
const { watchInstanceById } = client()
watchInstanceById(instanceId, (r) => {
console.log(`Handling instance update`, r)
dbg(`Handling instance update`, r)
const { action, record } = r
assertExists(record, `Expected instance here`)
instance.set(record)

View File

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

View File

@ -6,6 +6,7 @@
import { PUBLIC_PB_DOMAIN } from '$src/env'
import { client } from '$src/pocketbase'
import { createCleanupManagerSync } from '$util/CleanupManager'
import { error } from '$util/logger'
import { humanVersion, type InstanceRecordById, type InstancesRecord } from '@pockethost/common'
import { forEach, values } from '@s-libs/micro-dash'
import { onDestroy, onMount } from 'svelte'
@ -51,7 +52,7 @@
})
})
.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 { client } from '$src/pocketbase'
import { InstanceStatus, LATEST_PLATFORM, USE_LATEST_VERSION } from '@pockethost/common'
import { dbg, error, warn } from './logger'
export type FormErrorHandler = (value: string) => void
export const handleFormError = (error: any, setError?: FormErrorHandler) => {
const { parseError } = client()
console.error(`Form error: ${error}`, { error })
error(`Form error: ${error}`, { error })
if (setError) {
const message = parseError(error)[0]
@ -177,10 +178,10 @@ export const handleInstanceGeneratorWidget = async (
// populated the form with their existing login. Try using it.
await handleLogin(email, password, undefined, false)
.then(() => {
console.log(`Account ${email} already exists. Logged in.`)
dbg(`Account ${email} already exists. Logged in.`)
})
.catch((e) => {
console.warn(`Login failed, attempting account creation.`)
warn(`Login failed, attempting account creation.`)
// This means login has failed.
// Either their credentials were incorrect, or the account
// did not exist, or there is a system issue.
@ -188,15 +189,15 @@ export const handleInstanceGeneratorWidget = async (
// is already in use.
return handleRegistration(email, password)
.then(() => {
console.log(`Account created, proceeding to log in.`)
dbg(`Account created, proceeding to log in.`)
// This means registration succeeded. That's good.
// Log in using the new credentials
return handleLogin(email, password, undefined, false)
.then(() => {
console.log(`Logged in after account creation`)
dbg(`Logged in after account creation`)
})
.catch((e) => {
console.error(`Panic, auth system down`)
error(`Panic, auth system down`)
// This should never happen.
// If registration succeeds, login should always succeed.
// If a login fails at this point, the system is broken.
@ -206,7 +207,7 @@ export const handleInstanceGeneratorWidget = async (
})
})
.catch((e) => {
console.warn(`User input error`)
warn(`User input error`)
// This is just for clarity
// If registration fails at this point, it means both
// 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
// provided by the user.
// Instance creation could still fail if the name is taken
await handleCreateNewInstance(instanceName)
.then(() => {
console.log(`Creation of ${instanceName} succeeded`)
dbg(`Creation of ${instanceName} succeeded`)
})
.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.
// In any case, bail out to show errors.
if (e.data?.data?.subdomain?.code === 'validation_not_unique') {
@ -240,7 +241,7 @@ export const handleInstanceGeneratorWidget = async (
throw new Error(`Instance creation: ${messages[0]}`)
})
} catch (error: any) {
console.error(`Caught widget error`, { error })
error(`Caught widget error`, { error })
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 type { AuthStoreProps } from '$src/pocketbase/PocketbaseClient'
import { writable } from 'svelte/store'
import { dbg } from './logger'
export const authStoreState = writable<AuthStoreProps>({ isValid: false, model: null, token: '' })
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.
*/
onAuthChange((authStoreProps) => {
console.log(`onAuthChange in store`, { ...authStoreProps })
dbg(`onAuthChange in store`, { ...authStoreProps })
authStoreState.set(authStoreProps)
isAuthStateInitialized.set(true)
})
// Update derived stores when authStore changes
authStoreState.subscribe((authStoreProps) => {
console.log(`subscriber change`, authStoreProps)
dbg(`subscriber change`, authStoreProps)
isUserLoggedIn.set(authStoreProps.isValid)
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: more helpful error message when backup fails for nonexistent instance
- [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: rebuild with go 1.19.3 and include in bin name
- [ ] fix: Disallow backups if data dir doesn't exist