@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.
- package/.husky/pre-commit +1 -0
- package/.vscode/settings.json +3 -0
- package/README.md +3 -0
- package/eslint.config.js +73 -0
- package/package.json +40 -0
- package/source/Failure.ts +3 -0
- package/source/Method.ts +99 -0
- package/source/Octets.ts +15 -0
- package/source/Resource.ts +18 -0
- package/source/events.ts +20 -0
- package/source/fail.ts +13 -0
- package/source/index.ts +7 -0
- package/source/ok.ts +15 -0
- package/source/request.ts +74 -0
- package/source/settings.ts +12 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
npm test
|
package/README.md
ADDED
package/eslint.config.js
ADDED
|
@@ -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
|
+
}
|
package/source/Method.ts
ADDED
|
@@ -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
|
+
}
|
package/source/Octets.ts
ADDED
|
@@ -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
|
+
}
|
package/source/events.ts
ADDED
|
@@ -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
|
+
}
|
package/source/index.ts
ADDED
|
@@ -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
|
+
}
|
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
|
+
}
|