@toa.io/origin 0.0.1

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.
@@ -0,0 +1 @@
1
+ npm test
@@ -0,0 +1,3 @@
1
+ {
2
+ "typescript.tsdk": "node_modules/typescript/lib"
3
+ }
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # Toa Origin
2
+
3
+ [Exposition](https://github.com/toa-io/toa/tree/dev/extensions/exposition) Client
@@ -0,0 +1,73 @@
1
+ 'use strict'
2
+
3
+ const importPlugin = require('eslint-plugin-import')
4
+ const neostandard = require('neostandard')
5
+
6
+ module.exports = [
7
+ ...neostandard({
8
+ ts: true,
9
+ ignores: neostandard.resolveIgnoresFromGitignore(),
10
+ }),
11
+ {
12
+ plugins: {
13
+ import: importPlugin
14
+ }
15
+ },
16
+ {
17
+ rules: {
18
+ curly: ['error', 'multi'],
19
+ '@stylistic/space-before-function-paren': ['error', 'never'],
20
+ 'padding-line-between-statements': [
21
+ 'error',
22
+ {
23
+ blankLine: 'always',
24
+ prev: ['block-like', 'if'],
25
+ next: '*'
26
+ },
27
+ {
28
+ blankLine: 'always',
29
+ prev: '*',
30
+ next: ['block-like', 'if']
31
+ },
32
+ {
33
+ blankLine: 'always',
34
+ prev: ['const', 'let'],
35
+ next: ['expression', 'for']
36
+ },
37
+ {
38
+ blankLine: 'always',
39
+ prev: 'expression',
40
+ next: ['const', 'let']
41
+ },
42
+ {
43
+ blankLine: 'always',
44
+ prev: ['multiline-const', 'multiline-let'],
45
+ next: '*'
46
+ },
47
+ {
48
+ blankLine: 'always',
49
+ prev: '*',
50
+ next: ['multiline-const', 'multiline-let']
51
+ },
52
+ {
53
+ blankLine: 'always',
54
+ prev: '*',
55
+ next: 'return'
56
+ }
57
+ ],
58
+ 'import/order': ['error', {
59
+ groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'type'],
60
+ alphabetize: {
61
+ order: 'asc'
62
+ }
63
+ }],
64
+ },
65
+ },
66
+ {
67
+ files: ['**/*.ts'],
68
+ rules: {
69
+ 'no-void': ['error', { allowAsStatement: true }],
70
+ '@typescript-eslint/consistent-type-imports': 'error'
71
+ }
72
+ },
73
+ ]
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@toa.io/origin",
3
+ "version": "0.0.1",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "description": "Toa Origin",
8
+ "homepage": "git@github.com:toa-io/origin.git#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git@github.com:toa-io/origin.git"
12
+ },
13
+ "bugs": {
14
+ "url": "git@github.com:toa-io/origin.git/issues"
15
+ },
16
+ "main": "transpiled/index.js",
17
+ "types": "transpiled/index.d.ts",
18
+ "peerDependencies": {
19
+ "typescript": "^5.8.3"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^22.15.17",
23
+ "eslint": "^9.26.0",
24
+ "eslint-plugin-import": "^2.31.0",
25
+ "husky": "^9.1.7",
26
+ "neostandard": "^0.12.1"
27
+ },
28
+ "scripts": {
29
+ "prepare": "husky",
30
+ "prepublishOnly": "rm -rf transpiled && tsc",
31
+ "lint": "eslint .",
32
+ "format": "eslint . --fix",
33
+ "test": "node --test && npm run lint"
34
+ },
35
+ "dependencies": {
36
+ "error-value": "^0.4.4",
37
+ "meros": "^1.3.0",
38
+ "mitt": "^3.0.1"
39
+ }
40
+ }
@@ -0,0 +1,3 @@
1
+ import { Err } from 'error-value'
2
+
3
+ export class Failure extends Err<number> {}
@@ -0,0 +1,99 @@
1
+ import { Err } from 'error-value'
2
+ import mitt, { type Emitter } from 'mitt'
3
+ import { request, type Options } from './request'
4
+ import type { Failure } from './Failure'
5
+ import type { OctetsEntry, WorkflowStep } from './Octets'
6
+
7
+ export class Method<Entity = never> {
8
+ private readonly base: string
9
+ private readonly options?: Options
10
+
11
+ constructor(base: string, options?: Options) {
12
+ this.base = base
13
+ this.options = options
14
+ }
15
+
16
+ public async none(segments?: string[] | string, options?: Options): Promise<void | Failure> {
17
+ return await this.request(segments, options)
18
+ }
19
+
20
+ public async value<T = Entity>(segments?: string[] | string, options?: Options): Promise<T | Failure> {
21
+ return await this.request<T>(segments, options)
22
+ }
23
+
24
+ public async array<T = Entity[]>(segments?: string[] | string, options?: Options): Promise<T[] | Failure> {
25
+ return await this.request<T[]>(segments, options)
26
+ }
27
+
28
+ public async multipart<T = Entity>(
29
+ segments?: string[] | string,
30
+ options?: Options
31
+ ): Promise<AsyncGenerator<T, void, undefined> | Failure> {
32
+ const generator = await this.request<AsyncGenerator<{ body: string }>>(segments, options)
33
+
34
+ if (generator instanceof Error) return generator
35
+
36
+ const ack = await generator.next()
37
+
38
+ if (JSON.parse(ack.value.body) !== 'ACK') throw new Error('No ACK')
39
+
40
+ return (async function * () {
41
+ for await (const chunk of generator) {
42
+ const value = JSON.parse(chunk.body)
43
+
44
+ if (value === 'FIN') return
45
+
46
+ yield value
47
+ }
48
+ })()
49
+ }
50
+
51
+ public async octets<Events extends Record<string, unknown>>(
52
+ segments?: string[],
53
+ options?: Options
54
+ ): Promise<[OctetsEntry, Emitter<Events>] | Failure> {
55
+ const generator = await this.multipart(segments, options)
56
+
57
+ if (generator instanceof Error) return generator
58
+
59
+ const chunk = await generator.next()
60
+ const entry = chunk.value as OctetsEntry
61
+ const emitter = mitt<Events>()
62
+
63
+ void (async() => {
64
+ for await (const part of generator) {
65
+ const workflow = part as WorkflowStep
66
+
67
+ console.debug('octets chunk:', workflow)
68
+
69
+ const payload =
70
+ workflow.error === undefined
71
+ ? workflow.output
72
+ : new Err(workflow.error.code, workflow.error.message)
73
+
74
+ emitter.emit(workflow.step, payload as Events[typeof workflow.step])
75
+ }
76
+
77
+ emitter.off('*')
78
+ })()
79
+
80
+ return [entry, emitter]
81
+ }
82
+
83
+ private async request<T>(segments?: string | string[], options?: Options): Promise<T | Failure> {
84
+ const path = toPath(segments)
85
+
86
+ return await request<T>(`${this.base}${path}`, {
87
+ ...this.options,
88
+ ...options
89
+ })
90
+ }
91
+ }
92
+
93
+ function toPath(segments: string | string[] | undefined): string {
94
+ if (segments === undefined) return ''
95
+
96
+ if (typeof segments === 'string') return segments
97
+
98
+ return segments.filter((s) => s !== undefined).join('/') + '/'
99
+ }
@@ -0,0 +1,15 @@
1
+ export interface OctetsEntry {
2
+ id: string
3
+ }
4
+
5
+ export interface WorkflowStep<K = string, T = unknown> {
6
+ step: K
7
+ status: 'completed' | 'exception'
8
+ output?: T
9
+ error: WorkflowError
10
+ }
11
+
12
+ interface WorkflowError {
13
+ code: string
14
+ message?: string
15
+ }
@@ -0,0 +1,18 @@
1
+ import { Method } from './Method'
2
+ import type { Options } from './request'
3
+
4
+ export class Resource<T> {
5
+ public get: Method<T>
6
+ public post: Method<T>
7
+ public patch: Method<T>
8
+ public put: Method<T>
9
+ public delete: Method<never>
10
+
11
+ public constructor(base: string, options?: Omit<Options, 'method'>) {
12
+ this.get = new Method<T>(base, { method: 'GET', ...options })
13
+ this.post = new Method<T>(base, { method: 'POST', ...options })
14
+ this.patch = new Method<T>(base, { method: 'PATCH', ...options })
15
+ this.put = new Method<T>(base, { method: 'PUT', ...options })
16
+ this.delete = new Method(base, { method: 'DELETE', ...options })
17
+ }
18
+ }
@@ -0,0 +1,20 @@
1
+ import mitt from 'mitt'
2
+ import type { Options } from './request'
3
+
4
+ export const events = mitt<Events>()
5
+
6
+ export interface Events extends Record<string | symbol, unknown> {
7
+ challenge: string
8
+
9
+ request: {
10
+ id: string
11
+ path: string
12
+ options: Options
13
+ }
14
+
15
+ response: {
16
+ id: string
17
+ status: number
18
+ duration: number
19
+ }
20
+ }
package/source/fail.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { Failure } from './Failure'
2
+ import { events } from './events'
3
+
4
+ export async function fail(response: Response): Promise<Failure> {
5
+ const payload =
6
+ response.headers.get('content-type') === 'application/json'
7
+ ? await response.json()
8
+ : await response.text()
9
+
10
+ events.emit(response.status, payload)
11
+
12
+ return new Failure(response.status)
13
+ }
@@ -0,0 +1,7 @@
1
+ export { Resource } from './Resource'
2
+ export { Method } from './Method'
3
+ export { Failure } from './Failure'
4
+ export { authenticate, request, use, type Options } from './request'
5
+ export { events } from './events'
6
+ export { connect } from './settings'
7
+ export type * from './Octets'
package/source/ok.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { meros } from 'meros/browser'
2
+ import { events } from './events'
3
+
4
+ export async function ok<T>(response: Response): Promise<T> {
5
+ const challenge = response.headers.get('authorization')
6
+
7
+ if (challenge !== null) events.emit('challenge', challenge)
8
+
9
+ const type = response.headers.get('content-type')
10
+
11
+ if (type?.startsWith('multipart/')) return (await meros(response)) as T
12
+ else if (type === 'application/json') return (await response.json()) as T
13
+ else if (type?.startsWith('text/')) return (await response.text()) as T
14
+ else return (await response.blob()) as T
15
+ }
@@ -0,0 +1,74 @@
1
+ import { events } from './events'
2
+ import { fail } from './fail'
3
+ import { ok } from './ok'
4
+ import { settings } from './settings'
5
+ import type { Failure } from './Failure'
6
+
7
+ const delay = settings.delay
8
+
9
+ if (delay) console.warn(`API delay is enabled (${delay}ms)`)
10
+
11
+ let challenge: string | null = null
12
+
13
+ export function authenticate(value: string | null) {
14
+ challenge = value
15
+ }
16
+
17
+ let _fetch = fetch
18
+
19
+ export function use(fetcher: typeof fetch) {
20
+ _fetch = fetcher
21
+ }
22
+
23
+ export async function request<T = unknown>(
24
+ path: string,
25
+ options: Options = {}
26
+ ): Promise<T | Failure> {
27
+ options.headers ??= {}
28
+ options.headers['accept'] ??= 'application/json'
29
+
30
+ if (delay)
31
+ options.headers['sleep'] = Math.round((Math.random() * delay) / 2 + delay / 2).toString()
32
+
33
+ const authentication = options.credentials === 'include'
34
+
35
+ if (options.body !== undefined)
36
+ if (options.body instanceof File || options.body instanceof ReadableStream) {
37
+ options.method ??= 'POST'
38
+ options.duplex = 'half'
39
+ options.headers['content-type'] ??= 'application/octet-stream'
40
+ } else {
41
+ options.body = JSON.stringify(options.body)
42
+ options.headers['content-type'] ??= 'application/json'
43
+ }
44
+
45
+ if (authentication && options.headers['authorization'] === undefined) {
46
+ if (challenge === null)
47
+ throw new Error(`Credentials must be set before sending authenticated request ${options.method ?? 'GET'} ${path}`)
48
+
49
+ options.headers['authorization'] = challenge
50
+ delete options.credentials // no cookies
51
+ }
52
+
53
+ const start = performance.now()
54
+
55
+ const id =
56
+ typeof window !== 'undefined' && (window.crypto.randomUUID?.() ?? Math.random().toString())
57
+
58
+ if (id) events.emit('request', { id, path, options })
59
+
60
+ const response = await _fetch(settings.origin + path, options)
61
+
62
+ if (id)
63
+ events.emit('response', { id, status: response.status, duration: performance.now() - start })
64
+
65
+ if (!response.ok) return await fail(response)
66
+ else return await ok<T>(response)
67
+ }
68
+
69
+ export interface Options extends Omit<RequestInit, 'headers'> {
70
+ duplex?: 'half'
71
+
72
+ body?: any
73
+ headers?: Record<string, string>
74
+ }
@@ -0,0 +1,12 @@
1
+ const settings: Partial<Settings> = {} as const
2
+
3
+ export function connect(set: Settings) {
4
+ Object.assign(settings, set)
5
+ }
6
+
7
+ interface Settings {
8
+ origin: string
9
+ delay: number
10
+ }
11
+
12
+ export { settings }
package/tsconfig.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "outDir": "./transpiled",
4
+ "rootDir": "./source",
5
+ "module": "commonjs",
6
+ "moduleResolution": "node",
7
+ "target": "ESNext",
8
+ "sourceMap": true,
9
+ "declaration": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "incremental": true,
13
+ "strict": true,
14
+ "experimentalDecorators": true,
15
+ "noImplicitOverride": true,
16
+ "allowJs": true,
17
+ "forceConsistentCasingInFileNames": true
18
+ },
19
+ "include": [
20
+ "source/**/*"
21
+ ]
22
+ }