pockethost 1.0.0-rc.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/common.ts +1 -0
  2. package/core.ts +2 -0
  3. package/package.json +24 -112
  4. package/readme.md +36 -146
  5. package/src/cli/commands/ConfigCommand/index.ts +99 -0
  6. package/src/cli/commands/PluginCommand/index.ts +123 -0
  7. package/src/cli/commands/PluginCommand/util.ts +140 -0
  8. package/src/cli/commands/ServeCommand/index.ts +20 -0
  9. package/src/cli/index.ts +39 -0
  10. package/src/common/CleanupManager.ts +65 -0
  11. package/src/common/Logger.ts +168 -0
  12. package/src/common/ResourceAllocator.ts +49 -0
  13. package/src/common/TimerManager.ts +64 -0
  14. package/src/common/assert.ts +26 -0
  15. package/src/common/debug.ts +8 -0
  16. package/src/common/events.ts +53 -0
  17. package/src/common/index.ts +12 -0
  18. package/src/common/mergeConfig.ts +27 -0
  19. package/src/common/mkSingleton.ts +28 -0
  20. package/src/common/plugin/action.ts +60 -0
  21. package/src/common/plugin/filter.ts +65 -0
  22. package/src/common/plugin/helpers.ts +17 -0
  23. package/src/common/plugin/index.ts +27 -0
  24. package/src/common/pocketbase-client-helpers/RestHelper.ts +69 -0
  25. package/src/common/pocketbase-client-helpers/index.ts +1 -0
  26. package/src/common/promiseSerializer.ts +22 -0
  27. package/src/common/schema/Instance.ts +45 -0
  28. package/src/common/schema/InstanceLog.ts +12 -0
  29. package/src/common/schema/Rest/CreateInstance.ts +20 -0
  30. package/src/common/schema/Rest/DeleteInstance.ts +21 -0
  31. package/src/common/schema/Rest/GetUserTokenInfo.ts +22 -0
  32. package/src/common/schema/Rest/UpdateInstance.ts +61 -0
  33. package/src/common/schema/Rest/index.ts +23 -0
  34. package/src/common/schema/User.ts +24 -0
  35. package/src/common/schema/index.ts +6 -0
  36. package/src/common/schema/types.ts +18 -0
  37. package/src/common/schema/util.ts +1 -0
  38. package/src/constants.ts +74 -0
  39. package/src/core/Settings.ts +119 -0
  40. package/src/core/asyncExecutionGuard.ts +49 -0
  41. package/src/core/exit.ts +11 -0
  42. package/src/core/index.ts +7 -0
  43. package/src/core/internal.ts +3 -0
  44. package/src/core/smartFetch.ts +32 -0
  45. package/src/core/tryFetch.ts +66 -0
  46. package/src/server.ts +119 -0
  47. package/dist/index.mjs +0 -3156
  48. package/dist/instance-app/migrations/1699198734_pockethost_defaults.js +0 -39
  49. package/dist/instance-app/pb_hooks/_ph_admin_sync.pb.js +0 -71
  50. package/dist/instance-app/pb_hooks/_ph_lib.js +0 -19
  51. package/dist/instance-app/tsconfig.json +0 -8
  52. package/dist/instance-app/types/goja.d.ts +0 -58
  53. package/dist/instance-app/types/lib.d.ts +0 -5
  54. package/dist/instance-app/types/types.d.ts +0 -19573
  55. package/dist/mothership-app/migrations/1705062576_collections_snapshot.js +0 -1863
  56. package/dist/mothership-app/migrations/1705083826_updated_stats.js +0 -721
  57. package/dist/mothership-app/migrations/1706060446_updated_instances.js +0 -44
  58. package/dist/mothership-app/pb_hooks/src/instances-create.pb.js +0 -65
  59. package/dist/mothership-app/pb_hooks/src/instances-delete.pb.js +0 -69
  60. package/dist/mothership-app/pb_hooks/src/instances-migrate-versions.pb.js +0 -33
  61. package/dist/mothership-app/pb_hooks/src/instances-notify-discord-after-create.pb.js +0 -31
  62. package/dist/mothership-app/pb_hooks/src/instances-notify-on-maintenance-mode.pb.js +0 -52
  63. package/dist/mothership-app/pb_hooks/src/instances-reset-idle.pb.js +0 -7
  64. package/dist/mothership-app/pb_hooks/src/instances-update.pb.js +0 -121
  65. package/dist/mothership-app/pb_hooks/src/instances-validate-before-create.pb.js +0 -16
  66. package/dist/mothership-app/pb_hooks/src/instances-validate-before-update.pb.js +0 -43
  67. package/dist/mothership-app/pb_hooks/src/lib.js +0 -171
  68. package/dist/mothership-app/pb_hooks/src/ls.pb.js +0 -159
  69. package/dist/mothership-app/pb_hooks/src/mail.pb.js +0 -49
  70. package/dist/mothership-app/pb_hooks/src/notifications.pb.js +0 -62
  71. package/dist/mothership-app/pb_hooks/src/random-words.js +0 -2085
  72. package/dist/mothership-app/pb_hooks/src/signup.pb.js +0 -214
  73. package/dist/mothership-app/pb_hooks/src/sns.pb.js +0 -133
  74. package/dist/mothership-app/pb_hooks/src/unsubscribe.pb.js +0 -33
  75. package/dist/mothership-app/pb_hooks/src/user-token.pb.js +0 -33
  76. package/dist/mothership-app/pb_hooks/src/user-welcome-msg.pb.js +0 -32
  77. package/dist/mothership-app/pb_hooks/src/versions-route.pb.js +0 -12
  78. package/dist/mothership-app/pb_hooks/src/versions.js +0 -0
  79. package/dist/mothership-app/pb_hooks/tsconfig.json +0 -8
  80. package/dist/mothership-app/pb_hooks/types/global.d.ts +0 -1
  81. package/dist/mothership-app/pb_hooks/types/goja.d.ts +0 -58
  82. package/dist/mothership-app/pb_hooks/types/lemon.d.ts +0 -12
  83. package/dist/mothership-app/pb_hooks/types/lib.d.ts +0 -55
  84. package/dist/mothership-app/pb_hooks/types/types.d.ts +0 -19573
  85. package/dist/mothership-app/ph_app/index.js +0 -11
  86. package/dist/mothership-app/ph_app/package-lock.json +0 -657
  87. package/dist/mothership-app/ph_app/package.json +0 -14
@@ -0,0 +1,140 @@
1
+ import { exec } from 'child_process'
2
+ import fs from 'fs/promises'
3
+ import { Listr, ListrTask } from 'listr2'
4
+ import { createRequire } from 'module'
5
+ import fetch from 'node-fetch'
6
+ import semver from 'semver'
7
+ import util from 'util'
8
+
9
+ const execPromise = util.promisify(exec)
10
+ const require = createRequire(import.meta.url)
11
+
12
+ export async function getPackageJson(packageName: string): Promise<any | null> {
13
+ try {
14
+ const packageJsonPath = require.resolve(`${packageName}/package.json`)
15
+ const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'))
16
+ return packageJson
17
+ } catch (error) {
18
+ return null
19
+ }
20
+ }
21
+
22
+ export async function fetchPackageData(
23
+ packageName: string,
24
+ ): Promise<any | null> {
25
+ const npmRegistryUrl = `https://registry.npmjs.org/${packageName}`
26
+ try {
27
+ const response = await fetch(npmRegistryUrl)
28
+ return await response.json()
29
+ } catch (error) {
30
+ return null
31
+ }
32
+ }
33
+
34
+ async function installPackage(
35
+ packageName: string,
36
+ version: string,
37
+ ): Promise<void> {
38
+ try {
39
+ await execPromise(`npm install -g ${packageName}@${version}`)
40
+ } catch (e) {
41
+ throw new Error(`Error installing ${packageName}@${version}: ${e}`)
42
+ }
43
+ }
44
+
45
+ export async function removePackage(packageName: string): Promise<void> {
46
+ try {
47
+ await execPromise(`npm remove -g ${packageName}`)
48
+ } catch (e) {
49
+ throw new Error(`Error removing ${packageName}: ${e}`)
50
+ }
51
+ }
52
+
53
+ async function checkPackageCompatibility(
54
+ packageA: string,
55
+ versionA: string,
56
+ packageB: string,
57
+ ): Promise<string> {
58
+ const localPackageJson = await getPackageJson(packageB)
59
+
60
+ if (localPackageJson) {
61
+ const { version: localPackageBVersion, dependencies } = localPackageJson
62
+
63
+ if (dependencies && semver.satisfies(versionA, dependencies[packageA])) {
64
+ return `Local ${packageB}@${localPackageBVersion} is already compatible with ${packageA}@${versionA}`
65
+ }
66
+ }
67
+
68
+ const packageBData = await fetchPackageData(packageB)
69
+ if (!packageBData) {
70
+ throw new Error(`Error fetching data for ${packageB}`)
71
+ }
72
+
73
+ const versions = packageBData.versions
74
+ let latestCompatibleVersion: string | null = null
75
+
76
+ for (const version in versions) {
77
+ const dependencies = versions[version].dependencies
78
+ if (
79
+ dependencies &&
80
+ packageA in dependencies &&
81
+ semver.satisfies(versionA, dependencies[packageA])
82
+ ) {
83
+ if (
84
+ !latestCompatibleVersion ||
85
+ semver.gt(version, latestCompatibleVersion)
86
+ ) {
87
+ latestCompatibleVersion = version
88
+ }
89
+ }
90
+ }
91
+
92
+ if (latestCompatibleVersion) {
93
+ await installPackage(packageB, latestCompatibleVersion)
94
+ return `${packageB}@${latestCompatibleVersion} installed successfully`
95
+ } else {
96
+ throw new Error(
97
+ `No compatible version of ${packageB} found for ${packageA}@${versionA}`,
98
+ )
99
+ }
100
+ }
101
+
102
+ export async function getCompatibleVersions(
103
+ packageA: string,
104
+ versionA: string,
105
+ packagesB: string[],
106
+ ): Promise<void> {
107
+ const tasks = packagesB.map(
108
+ (packageB) =>
109
+ ({
110
+ title: `Checking ${packageB}...`,
111
+ task: async (_, task) => {
112
+ try {
113
+ const result = await checkPackageCompatibility(
114
+ packageA,
115
+ versionA,
116
+ packageB,
117
+ )
118
+ task.output = result
119
+ } catch (error) {
120
+ task.output = `${error}`
121
+ throw error
122
+ }
123
+ },
124
+ }) as ListrTask,
125
+ )
126
+
127
+ const listr = new Listr(tasks, {
128
+ concurrent: true,
129
+ rendererOptions: { collapseSubtasks: false },
130
+ })
131
+
132
+ await listr.run()
133
+ }
134
+
135
+ // Example usage
136
+ // const packageA = 'express'
137
+ // const versionA = '4.17.1'
138
+ // const packagesB = ['some-package', 'another-package', 'yet-another-package']
139
+
140
+ // getCompatibleVersions(packageA, versionA, packagesB)
@@ -0,0 +1,20 @@
1
+ import { Command } from 'commander'
2
+ import { LoggerService } from '../../../common'
3
+ import { serve } from '../../../server'
4
+
5
+ type Options = {
6
+ isolate: boolean
7
+ }
8
+
9
+ export const ServeCommand = () => {
10
+ const cmd = new Command(`serve`)
11
+ .description(`Run PocketHost`)
12
+ .action(async (options: Options) => {
13
+ const logger = LoggerService().create(`ServeCommand`)
14
+ const { dbg, error, info, warn } = logger
15
+ info(`Starting`)
16
+
17
+ await serve()
18
+ })
19
+ return cmd
20
+ }
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env tsx
2
+
3
+ import { uniq } from '@s-libs/micro-dash'
4
+ import { Command, program } from 'commander'
5
+ import {
6
+ DEBUG,
7
+ LogLevelName,
8
+ LoggerService,
9
+ PH_PLUGINS,
10
+ PocketHostFilter,
11
+ filter,
12
+ loadPlugins,
13
+ } from '../../core'
14
+ import { ConfigCommand } from './commands/ConfigCommand'
15
+ import { PluginCommand } from './commands/PluginCommand'
16
+ import { ServeCommand } from './commands/ServeCommand'
17
+
18
+ export const { dbg, info, error } = LoggerService({
19
+ level: DEBUG() ? LogLevelName.Debug : LogLevelName.Info,
20
+ })
21
+
22
+ export const main = async () => {
23
+ await loadPlugins(uniq(PH_PLUGINS()))
24
+
25
+ program.name('pockethost').description('Multitenant PocketBase hosting')
26
+
27
+ const commands = await filter<Command[]>(PocketHostFilter.Core_CliCommands, [
28
+ ServeCommand(),
29
+ ConfigCommand(),
30
+ PluginCommand(),
31
+ ])
32
+ for (const command of commands) {
33
+ program.addCommand(command)
34
+ }
35
+
36
+ await program.parseAsync()
37
+ }
38
+
39
+ main()
@@ -0,0 +1,65 @@
1
+ import { reduce, uniqueId, values } from '@s-libs/micro-dash'
2
+ import { LoggerService } from '.'
3
+
4
+ export type CleanupFunc = () => Promise<void> | void
5
+
6
+ type CleanupRec = {
7
+ cleanup: CleanupFunc
8
+ priority: number
9
+ }
10
+
11
+ export const CLEANUP_DEFAULT_PRIORITY = 10
12
+ export const CLEANUP_PRIORITY_HIGH = 1
13
+ export const CLEANUP_PRIORITY_LOW = 20
14
+ export const CLEANUP_PRIORITY_LAST = 1000
15
+
16
+ export type CleanupManagerApi = ReturnType<typeof createCleanupManager>
17
+
18
+ export const createCleanupManager = (slug?: string) => {
19
+ const _slug = slug || uniqueId()
20
+
21
+ const { error, warn, dbg } = LoggerService().create(`cleanupManager:${_slug}`)
22
+
23
+ let i = 0
24
+
25
+ const cleanups: { [_: number]: CleanupRec } = {}
26
+
27
+ const add = (cb: CleanupFunc, priority = CLEANUP_DEFAULT_PRIORITY) => {
28
+ const idx = i++
29
+
30
+ const cleanup = async () => {
31
+ await cb()
32
+ delete cleanups[idx]
33
+ }
34
+
35
+ cleanups[idx] = { cleanup, priority }
36
+
37
+ return cleanup
38
+ }
39
+
40
+ let _shutdownP: Promise<void> | undefined = undefined
41
+
42
+ const shutdown: () => Promise<void> = () => {
43
+ if (_shutdownP) return _shutdownP
44
+
45
+ const _cleanupFuncs = values(cleanups)
46
+ .sort((a, b) => a.priority - b.priority)
47
+ .map((v) => v.cleanup)
48
+ _shutdownP = reduce(
49
+ _cleanupFuncs,
50
+ (c, v) => {
51
+ return c.then(() => v())
52
+ },
53
+ Promise.resolve(),
54
+ ).catch((e) => {
55
+ error(
56
+ `Cleanup functions are failing. This should never happen, check all cleanup functions to make sure they are trapping their exceptions.`,
57
+ )
58
+ throw e
59
+ })
60
+
61
+ return _shutdownP
62
+ }
63
+
64
+ return { add, shutdown }
65
+ }
@@ -0,0 +1,168 @@
1
+ /// <require "node">
2
+
3
+ import chalk from 'chalk'
4
+ import stringify from 'json-stringify-safe'
5
+ import { PocketHostAction, action, mergeConfig, mkSingleton } from '.'
6
+
7
+ export type LoggerConfig = {
8
+ level: LogLevelName
9
+ pfx: string[]
10
+ }
11
+
12
+ export type Logger = ReturnType<typeof createLogger>
13
+
14
+ export const isLevelLte = (a: LogLevelName, b: LogLevelName) => {
15
+ return LogLevels[a] <= LogLevels[b]
16
+ }
17
+
18
+ export const isLevelGt = (a: LogLevelName, b: LogLevelName) => {
19
+ return LogLevels[a] > LogLevels[b]
20
+ }
21
+
22
+ export const isLevelGte = (a: LogLevelName, b: LogLevelName) => {
23
+ return LogLevels[a] >= LogLevels[b]
24
+ }
25
+
26
+ export enum LogLevelName {
27
+ Trace = 'trace',
28
+ Raw = 'raw',
29
+ Debug = 'debug',
30
+ Info = 'info',
31
+ Warn = 'warn',
32
+ Error = 'error',
33
+ Abort = 'abort',
34
+ }
35
+
36
+ export const LEVEL_NAMES = [
37
+ LogLevelName.Trace,
38
+ LogLevelName.Raw,
39
+ LogLevelName.Debug,
40
+ LogLevelName.Info,
41
+ LogLevelName.Warn,
42
+ LogLevelName.Error,
43
+ LogLevelName.Abort,
44
+ ]
45
+
46
+ export const LogLevels = {
47
+ [LogLevelName.Trace]: 0,
48
+ [LogLevelName.Raw]: 1,
49
+ [LogLevelName.Debug]: 2,
50
+ [LogLevelName.Info]: 3,
51
+ [LogLevelName.Warn]: 4,
52
+ [LogLevelName.Error]: 5,
53
+ [LogLevelName.Abort]: 6,
54
+ } as const
55
+
56
+ export const createLogger = (config: Partial<LoggerConfig>) => {
57
+ const _config = mergeConfig<LoggerConfig>(
58
+ {
59
+ level: LogLevelName.Debug,
60
+ pfx: [''],
61
+ },
62
+ config,
63
+ )
64
+ const { pfx } = _config
65
+
66
+ const setLevel = (level: LogLevelName) => {
67
+ _config.level = level
68
+ }
69
+
70
+ const _pfx = (s: string) =>
71
+ [s, ...pfx]
72
+ .filter((v) => !!v)
73
+ .map((p) => `[${p}]`)
74
+ .join(' ')
75
+
76
+ const _log = (levelIn: LogLevelName, ...args: any[]) => {
77
+ action(PocketHostAction.Core_Log, _config.level, levelIn, args)
78
+ }
79
+
80
+ const raw = (...args: any[]) => {
81
+ _log(LogLevelName.Raw, _pfx('RAW'), ...args)
82
+ }
83
+
84
+ const trace = (...args: any[]) => {
85
+ _log(LogLevelName.Trace, _pfx(`TRACE`), ...args)
86
+ }
87
+
88
+ const dbg = (...args: any[]) => {
89
+ _log(LogLevelName.Debug, _pfx(chalk.blueBright('DBG')), ...args)
90
+ }
91
+
92
+ const info = (...args: any[]) => {
93
+ const _args = (() => {
94
+ if (isLevelGt(LogLevelName.Info, _config.level)) {
95
+ return [chalk.gray(`INFO`), ...args]
96
+ }
97
+ return [...args]
98
+ })()
99
+ _log(
100
+ LogLevelName.Info,
101
+
102
+ ..._args,
103
+ )
104
+ }
105
+
106
+ const warn = (...args: any[]) => {
107
+ _log(
108
+ LogLevelName.Warn,
109
+ _pfx(chalk.yellow(chalk.cyanBright('WARN'))),
110
+ ...args,
111
+ )
112
+ }
113
+
114
+ const error = (...args: any[]) => {
115
+ _log(LogLevelName.Error, ...[_pfx(chalk.bgRed(`ERROR`)), ...args])
116
+ }
117
+
118
+ const criticalError = (...args: any[]) => {
119
+ _log(LogLevelName.Error, ...[_pfx(chalk.bgRed(`ERROR`)), ...args])
120
+ new Error().stack?.split(/\n/).forEach((line) => {
121
+ _log(LogLevelName.Debug, _pfx(chalk.bgRed(`ERROR`)), line)
122
+ })
123
+ }
124
+
125
+ const abort = (...args: any[]): never => {
126
+ _log(LogLevelName.Abort, true, ...[_pfx(chalk.bgRed(`ABORT`)), ...args])
127
+ throw new Error(`Fatal error: ${stringify(args)}`)
128
+ }
129
+
130
+ const create = (s: string, configOverride?: Partial<LoggerConfig>) =>
131
+ createLogger({
132
+ ..._config,
133
+ ...configOverride,
134
+ pfx: [..._config.pfx, s],
135
+ })
136
+
137
+ const breadcrumb = (s: string) => {
138
+ pfx.push(s)
139
+ return api
140
+ }
141
+
142
+ // Compatibility func
143
+ const child = (extra: any) => create(stringify(extra))
144
+
145
+ const api = {
146
+ raw,
147
+ dbg,
148
+ warn,
149
+ info,
150
+ error,
151
+ criticalError,
152
+ create,
153
+ child,
154
+ trace,
155
+ debug: dbg,
156
+ breadcrumb,
157
+ abort,
158
+ shutdown() {
159
+ dbg(`Logger shutting down`)
160
+ },
161
+ setLevel,
162
+ }
163
+ return api
164
+ }
165
+
166
+ export const LoggerService = mkSingleton((config: Partial<LoggerConfig> = {}) =>
167
+ createLogger(config),
168
+ )
@@ -0,0 +1,49 @@
1
+ export type Deallocator = () => void
2
+
3
+ export type Allocator<T> = {
4
+ free: (item: T) => void
5
+ alloc: () => [T, Deallocator]
6
+ }
7
+
8
+ export function ResourceAllocator<T>(
9
+ initialPoolSize: number,
10
+ initializer: () => T,
11
+ ): Allocator<T> {
12
+ // Available queue to hold resolved items of type T
13
+ const available: T[] = []
14
+
15
+ // Function to add an item back to the available queue
16
+ const free = (item: T) => {
17
+ available.push(item)
18
+ }
19
+
20
+ // Pre-populate the available queue with initial items
21
+ for (let i = 0; i < initialPoolSize; i++) {
22
+ available.push(initializer())
23
+ }
24
+
25
+ return {
26
+ alloc() {
27
+ if (available.length === 0) {
28
+ available.push(initializer())
29
+ }
30
+ const newItem = initializer()
31
+ return [newItem, () => free(newItem)]
32
+ },
33
+ free,
34
+ }
35
+ }
36
+
37
+ /*
38
+ // Example Usage
39
+ const allocator = PromiseAllocator(2, async () => {
40
+ return new Promise<number>((resolve) => {
41
+ setTimeout(() => resolve(Math.random()), 1000);
42
+ });
43
+ });
44
+
45
+ allocator.alloc().then(([num, freeFunc]) => {
46
+ console.log(num); // e.g. 0.123456789
47
+ freeFunc(); // Adds the number back to the available queue
48
+ });
49
+ */
@@ -0,0 +1,64 @@
1
+ import { forEach, values } from '@s-libs/micro-dash'
2
+ import { LoggerService } from '.'
3
+
4
+ export type UnixTimestampMs = number
5
+ export type TimerCanceler = () => void
6
+
7
+ export type RepeatableTimerCallback = () => boolean | Promise<boolean>
8
+ export type TimerCallback = () => void | Promise<void>
9
+ export type TimeManagerConfig = {}
10
+
11
+ export type TimeManager = ReturnType<typeof createTimerManager>
12
+
13
+ export const createTimerManager = (config?: Partial<TimeManagerConfig>) => {
14
+ const { dbg, error } = LoggerService().create(`timerManager`)
15
+ let i = 0
16
+ const cleanups: any = {}
17
+ let isShutDown = false
18
+
19
+ const add = (cb: TimerCallback, ms: UnixTimestampMs) => {
20
+ if (isShutDown) throw new Error(`Already shut down`)
21
+ const idx = i++
22
+ const tid = setTimeout(async () => {
23
+ cancel()
24
+ try {
25
+ await cb()
26
+ } catch (e) {
27
+ error(e)
28
+ }
29
+ }, ms)
30
+ const cancel = () => {
31
+ clearTimeout(tid)
32
+ delete cleanups[idx]
33
+ }
34
+ cleanups[idx] = cancel
35
+ return cancel
36
+ }
37
+
38
+ const shutdown = () => {
39
+ isShutDown = true
40
+
41
+ dbg(`Canceling ${values(cleanups.length).length} timers`)
42
+ forEach(cleanups, (c) => c())
43
+ dbg(`done`, cleanups)
44
+ }
45
+
46
+ const repeat = (
47
+ cb: RepeatableTimerCallback,
48
+ ms: UnixTimestampMs,
49
+ initial = true,
50
+ ) => {
51
+ let _unsub: TimerCanceler | undefined = undefined
52
+ const _again = async () => {
53
+ const shouldRepeat = await cb()
54
+ if (shouldRepeat && !isShutDown) _unsub = add(_again, ms)
55
+ }
56
+ add(_again, initial ? 0 : ms)
57
+ return () => {
58
+ _unsub?.()
59
+ _unsub = undefined
60
+ }
61
+ }
62
+
63
+ return { add, shutdown, repeat }
64
+ }
@@ -0,0 +1,26 @@
1
+ export function assertExists<TType>(
2
+ v: TType,
3
+ message = `Value does not exist`,
4
+ ): asserts v is NonNullable<TType> {
5
+ if (typeof v === 'undefined') {
6
+ throw new Error(message)
7
+ }
8
+ }
9
+
10
+ export function assertTruthy<TType>(
11
+ v: unknown,
12
+ message = `Value should be truthy`,
13
+ ): asserts v is NonNullable<TType> {
14
+ if (!v) {
15
+ throw new Error(message)
16
+ }
17
+ }
18
+
19
+ export function assert<T>(
20
+ v: T | undefined | void | null,
21
+ msg?: string,
22
+ ): asserts v is T {
23
+ if (!v) {
24
+ throw new Error(msg || `Assertion failure`)
25
+ }
26
+ }
@@ -0,0 +1,8 @@
1
+ import { boolean } from 'boolean'
2
+
3
+ export const IS_DEV = () =>
4
+ `PH_DEV` in process.env
5
+ ? boolean(process.env.PH_DEV)
6
+ : process.env.NODE_ENV === 'development'
7
+ export const DEBUG = () =>
8
+ `PH_DEBUG` in process.env ? boolean(process.env.PH_DEBUG) : IS_DEV()
@@ -0,0 +1,53 @@
1
+ import { values } from '@s-libs/micro-dash'
2
+
3
+ export type Unsubscribe = () => void
4
+
5
+ export type EventSubscriber<TPayload> = (
6
+ cb: EventHandler<TPayload>,
7
+ ) => Unsubscribe
8
+ export type EventEmitter<TPayload> = (
9
+ payload: TPayload,
10
+ stopOnHandled?: boolean,
11
+ ) => Promise<boolean>
12
+ export type EventHandler<TPayload> = (
13
+ payload: TPayload,
14
+ isHandled: boolean,
15
+ ) => boolean | void | Promise<boolean | void>
16
+
17
+ /**
18
+ * @param defaultHandler Optional handler to call if no handler calls
19
+ * `handled()`
20
+ * @returns Void
21
+ */
22
+ export const createEvent = <TPayload>(
23
+ defaultHandler?: EventHandler<TPayload>,
24
+ ): [EventSubscriber<TPayload>, EventEmitter<TPayload>] => {
25
+ let i = 0
26
+ const callbacks: any = {}
27
+ let callbacksArray: EventHandler<TPayload>[] = []
28
+ const onEvent = (cb: EventHandler<TPayload>) => {
29
+ const id = i++
30
+ callbacks[id] = cb
31
+ callbacksArray = values(callbacks)
32
+ return () => {
33
+ delete callbacks[id]
34
+ }
35
+ }
36
+
37
+ const fireEvent = async (payload: TPayload, stopOnHandled = false) => {
38
+ let _handled = false
39
+ for (let i = 0; i < callbacksArray.length; i++) {
40
+ const cb = callbacksArray[i]
41
+ if (!cb) continue
42
+ const res: boolean = !!(await cb(payload, _handled))
43
+ _handled = _handled || res
44
+ if (stopOnHandled && _handled) break
45
+ }
46
+ if (!_handled) {
47
+ await defaultHandler?.(payload, false)
48
+ }
49
+ return _handled
50
+ }
51
+
52
+ return [onEvent, fireEvent]
53
+ }
@@ -0,0 +1,12 @@
1
+ export * from './CleanupManager'
2
+ export * from './Logger'
3
+ export * from './TimerManager'
4
+ export * from './assert'
5
+ export * from './debug'
6
+ export * from './events'
7
+ export * from './mergeConfig'
8
+ export * from './mkSingleton'
9
+ export * from './plugin'
10
+ export * from './pocketbase-client-helpers'
11
+ export * from './promiseSerializer'
12
+ export * from './schema'
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Merges a config object with defaults. Shallow copies root descendants,
3
+ * skipping`undefined` values.
4
+ *
5
+ * @param defaultConfig The default config values
6
+ * @param partialConfigs A partial config to merge with defaults
7
+ * @returns
8
+ */
9
+ export function mergeConfig<T>(
10
+ defaultConfig: T,
11
+ ...partialConfigs: Partial<T>[]
12
+ ): T {
13
+ const result: T = { ...defaultConfig }
14
+
15
+ for (const partialConfig of partialConfigs) {
16
+ for (const key in partialConfig) {
17
+ if (Object.prototype.hasOwnProperty.call(partialConfig, key)) {
18
+ const value = partialConfig[key as keyof typeof partialConfig]
19
+ if (value !== undefined) {
20
+ result[key as keyof T] = value as T[keyof T]
21
+ }
22
+ }
23
+ }
24
+ }
25
+
26
+ return result
27
+ }
@@ -0,0 +1,28 @@
1
+ export type SingletonApi = Object
2
+
3
+ export type SingletonBaseConfig = {}
4
+
5
+ export function mkSingleton<TConfig, TInstance>(
6
+ factory: (config: TConfig) => TInstance,
7
+ ): (config?: TConfig) => TInstance {
8
+ let instance: TInstance | undefined
9
+
10
+ return (config?: TConfig): TInstance => {
11
+ if (instance && config) {
12
+ console.warn('Attempted to initialize service twice.')
13
+ console.warn(new Error().stack)
14
+ return instance
15
+ }
16
+
17
+ if (!instance && !config) {
18
+ console.warn(new Error().stack)
19
+ throw new Error('Attempted to use service before initialization.')
20
+ }
21
+
22
+ if (!instance) {
23
+ instance = factory(config as TConfig)
24
+ }
25
+
26
+ return instance
27
+ }
28
+ }