@defjs/core 0.4.0-beta.0 → 0.4.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.
- package/README.md +64 -72
- package/index.d.ts +2280 -731
- package/index.js +5830 -4562
- package/index.min.js +38 -2
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# @defjs/core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Designed for better TypeScript SDKs: define reusable HTTP, SSE, and WebSocket APIs, then execute them with an application-owned client.
|
|
4
|
+
|
|
5
|
+
`def` means `define`. SDK authors export operation definitions and data contracts; consuming applications configure service addresses, authentication, interceptors, and transport. The definitions can live in a shared module or a published npm package without capturing a particular client instance.
|
|
4
6
|
|
|
5
7
|
This README describes the package version that contains it. Defjs is still pre-1.0, so use the documentation and release notes published for the exact version installed in your application.
|
|
6
8
|
|
|
@@ -15,39 +17,53 @@ The package is ESM and declares no runtime dependencies. Its tarball retains thi
|
|
|
15
17
|
## Quick Start
|
|
16
18
|
|
|
17
19
|
```typescript
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}),
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
20
|
+
// users-sdk.ts — SDK definition
|
|
21
|
+
import { defineRequest, struct } from "@defjs/core";
|
|
22
|
+
|
|
23
|
+
export const getUser = defineRequest({
|
|
24
|
+
method: "GET",
|
|
25
|
+
path: "/users/:id",
|
|
26
|
+
input: struct.object({ id: struct.number() }),
|
|
27
|
+
builder: (input) => ({ path: input }),
|
|
28
|
+
output: struct.object({ id: struct.number(), name: struct.string() }),
|
|
29
|
+
error: struct.object({ message: struct.string() }),
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The application imports the definition and supplies its own client:
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// app.ts
|
|
37
|
+
import { createClient, withEndpoint } from "@defjs/core";
|
|
38
|
+
import { getUser } from "./users-sdk.ts";
|
|
39
|
+
|
|
40
|
+
const client = createClient(withEndpoint("https://api.example.com"));
|
|
41
|
+
|
|
42
|
+
async function main() {
|
|
43
|
+
const command = getUser({ id: 1 });
|
|
44
|
+
const [error, user] = await client.execute(command);
|
|
45
|
+
if (error) {
|
|
46
|
+
console.error(String(error), error.code);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
console.log(user.id, user.name);
|
|
40
50
|
}
|
|
51
|
+
|
|
52
|
+
void main();
|
|
41
53
|
```
|
|
42
54
|
|
|
43
|
-
`
|
|
55
|
+
`output` is one Struct for the 2xx body; `error` is one Struct for everything else. `ok` is the only fork, and only one side ever decodes. Omit a side and that body is never read.
|
|
56
|
+
|
|
57
|
+
Best practice: `getUser({ id: 1 })` only creates a command. Give it a name, then pass that name to `client.execute`. The call validates, builds, and sends the request using the application's client. When `error` is set, return before reading the success value. When the definitions are published, import `getUser` from your SDK package instead of the local file. See [Build and publish an SDK](https://defjs.lyz.cloud/recipes/publish-http-sdk) for package exports, declarations, and compatible Core dependencies.
|
|
44
58
|
|
|
45
59
|
## Core Boundaries
|
|
46
60
|
|
|
47
|
-
-
|
|
48
|
-
- Structs validate request input and response data at runtime. `
|
|
49
|
-
- Expected
|
|
50
|
-
-
|
|
61
|
+
- `defineRequest`, `defineEventStream`, and `defineWebSocket` return reusable endpoints; calling an endpoint creates a typed command for `client.execute`. SDK and application share the Defjs execution contract and a compatible Core version.
|
|
62
|
+
- Structs validate request input and response data at runtime. A `builder` places parsed input onto `path`, `query`, `headers`, and `body`. Every definition with `input` requires an explicit `builder`; only definitions without `input` may omit it. `.alias(name)` is the wire name for a typed value.
|
|
63
|
+
- Expected failures are returned as native `Fault` instances in an error-first tuple; custom interceptors and application callbacks can still throw. A `Fault` is discriminated by one closed set of `code` values whose prefix is the class — `HTTP_STATUS`, `REQ_*`, `NET_*`, `RES_*`, `EXT_*`, `CAP_*`, `ENV_UNSUPPORTED`. `String(error)` renders the fault message; review record keys in structural paths and custom messages before logging. `code` and the variant metadata remain enumerable for structured logs.
|
|
64
|
+
- Default Struct messages name the received kind, never its value, for example `Expected number at email, received string`. `RES_DECODE_FAILED` from a JSON `SyntaxError` says `Response body could not be decoded`. Raw `StructIssue.received` and parser causes remain sensitive; custom `errorMap` and extension messages are used verbatim and need review before logging. Any string from `errorMap`, including `''`, replaces the default, also for bigint/date conversion failures from another Core copy.
|
|
65
|
+
- Faults carry their underlying value on the native `Error` cause chain. Narrow `error.cause instanceof StructError` before calling Struct-only helpers such as `format()`, `flatten()`, or `prettify()`.
|
|
66
|
+
- `isAbortCause(cause)` and `isTimeoutCause(cause)` recognize cancellation sentinels across Core copies through their non-enumerable `Symbol.for` brands. The former also recognizes a `DOMException` named `AbortError`; the latter recognizes a `DOMException` or `Error` named `TimeoutError`. Pass a fault's underlying `cause`, not the fault itself.
|
|
51
67
|
- Server clients that capture cookies, authorization, tenant data, or user data must be created inside the owning request scope.
|
|
52
68
|
- Ordinary HTTP work is request-scoped and is bounded with its execute-time timeout or `AbortSignal`; `Client` is not `AsyncDisposable` and has no global `dispose()` lifecycle.
|
|
53
69
|
- Returned SSE and WebSocket handles are `AsyncDisposable`, so `await using` waits for Defjs-owned teardown. SSE disposal stops Defjs reading/reconnect work and releases its reader lock, but does not wait forever for a provider-controlled `cancel()` promise.
|
|
@@ -57,69 +73,45 @@ if (error) {
|
|
|
57
73
|
|
|
58
74
|
For client-local tests, inject a Fetch-compatible function with `withHTTPHandle(...)`. This keeps request interception scoped to one client and still exercises command building, interceptors, status dispatch, and response validation.
|
|
59
75
|
|
|
60
|
-
### WebSocket
|
|
76
|
+
### WebSocket messages
|
|
61
77
|
|
|
62
|
-
`
|
|
78
|
+
`incoming` and `outgoing` are each one struct. `session.send` encodes that struct (aliases included) and sends JSON. A frame that is not valid JSON, or does not match `incoming`, is reported to `session.onRuntimeError` and is not delivered. Use `struct.discriminatedUnion` when one socket carries several message kinds. Static subprotocols belong on `protocols`.
|
|
63
79
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
'method.subscribe': struct.object({ method: struct.literal('subscribe'), success: struct.boolean() }),
|
|
68
|
-
},
|
|
69
|
-
maxIncomingQueueSize: 16,
|
|
70
|
-
normalizeIncoming(decoded) {
|
|
71
|
-
if (typeof decoded !== 'object' || decoded === null || Array.isArray(decoded)) return undefined
|
|
72
|
-
if (Reflect.get(decoded, 'method') !== 'subscribe') return undefined
|
|
73
|
-
return { data: decoded, type: 'method.subscribe' }
|
|
74
|
-
},
|
|
75
|
-
normalizeOutgoing(_type, encodedPayload) {
|
|
76
|
-
if (typeof encodedPayload !== 'object' || encodedPayload === null || Array.isArray(encodedPayload)) {
|
|
77
|
-
throw new TypeError('Expected encoded command')
|
|
78
|
-
}
|
|
79
|
-
return encodedPayload as { readonly [key: string]: unknown }
|
|
80
|
-
},
|
|
81
|
-
outgoing: {
|
|
82
|
-
subscribe: struct.object({
|
|
83
|
-
method: struct.literal('subscribe'),
|
|
84
|
-
params: struct.object({ channel: struct.string() }),
|
|
85
|
-
reqId: struct.number().alias('req_id'),
|
|
86
|
-
}),
|
|
87
|
-
},
|
|
88
|
-
path: '/v2',
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
// session.send({ type: 'subscribe', method: 'subscribe', params: { channel: 'ticker' }, reqId: 1 })
|
|
92
|
-
// wire: {"method":"subscribe","params":{"channel":"ticker"},"req_id":1}
|
|
93
|
-
```
|
|
80
|
+
After encoding, values JSON cannot faithfully represent throw `TypeError` before sending or entering the reconnect queue, including non-finite numbers, functions, symbols, `Map`, `Set`, streams, and class instances without a JSON representation. The same serializer protects heartbeat messages; a heartbeat serialization failure ends the session. If setting an injected handle's `binaryType` throws, Core requests best-effort closure before failing the attempt.
|
|
81
|
+
|
|
82
|
+
## Runtime Notes
|
|
94
83
|
|
|
95
|
-
|
|
84
|
+
Repository development, testing, and building use Bun `1.4.4`; package release jobs use `npm publish`. The package uses standard Web APIs such as `fetch`, `Request`, `Response`, `Headers`, streams, and `WebSocket` according to the transports enabled by the application. Repository type checking uses `bun check`; declaration files are generated by tsdown.
|
|
96
85
|
|
|
97
|
-
The
|
|
86
|
+
The default HTTP and SSE handles resolve the current global `fetch` on each call, including replacements installed after import. Importing Core without Fetch is valid; default execution reports `ENV_UNSUPPORTED` if it is still missing, while custom handles remain usable. HTTP uses `(url, init)` only for untouched platform Fetch; replaced globals and custom handles receive a `Request`.
|
|
98
87
|
|
|
99
|
-
|
|
88
|
+
Server-side FormData upload progress reads the serialized stream directly without whole-body Blob buffering and preserves its multipart boundary. The total is unknown unless a valid explicit `Content-Length` supplies it. Explicit content types survive body replacement and upload conversion, with FormData's actual boundary as the exception. Downloads with non-identity encoding report `lengthComputable: false` and `total: 0`; `loaded` counts stream bytes, which Fetch may already have decompressed. In a Window or Worker, XSRF injection checks the resolved origin against `location.origin`; cookie lookup separately requires `document`.
|
|
100
89
|
|
|
101
|
-
|
|
90
|
+
The supported minimum compiler/lib contract uses `target: 'ES2022'` with `lib: ['ES2022', 'ESNext.Disposable', 'DOM', 'DOM.Iterable']`. TypeScript 7 is the current repository tooling baseline; older compiler versions are not promised. The repository currently uses `ESNext` and does not automatically verify this minimum consumer lib set. These entries supply types only — not runtime Web API polyfills.
|
|
102
91
|
|
|
103
|
-
|
|
92
|
+
CI runs the Bun `verify` gate and Core's `core-server` and OpenTelemetry tests on Node 26. Package release jobs typecheck their build configurations and assert required files with `npm pack --dry-run`; they do not install tarballs into an external consumer for declaration or runtime verification.
|
|
104
93
|
|
|
105
|
-
**SSE reconnect behavior change:** without `withSSEReconnect(...)`, SSE no longer retries network/stream-read failures. Pass `withSSEReconnect({ attempts: 3 })` (or another reviewed budget) to retry those failures.
|
|
94
|
+
**SSE reconnect behavior change:** without `withSSEReconnect(...)`, SSE no longer retries network/stream-read failures. Pass `withSSEReconnect({ attempts: 3 })` (or another reviewed budget per outage) to retry those failures. SSE and WebSocket both reset the retry budget and backoff exponent after a successful handshake. `delayMs` must be finite and non-negative; `maxDelayMs` accepts a non-negative finite number or positive `Infinity`, which removes only the configured cap. The final delay after jitter and rounding is still bounded by `2_147_483_647` ms; zero bases stay zero and valid-number overflow saturates. Invalid numeric reconnect options fail before transport with `REQ_OPTIONS_INVALID`; WebSocket `attempts: 0` disables subsequent retries but still validates all explicitly supplied reconnect fields before the initial connection, as SSE does. SSE sends `Last-Event-ID` as UTF-8 bytes and strips only the physical stream's leading BOM, preserving U+FEFF in field values.
|
|
106
95
|
|
|
107
96
|
When using a checkout through `file:` or a workspace link, run `bun run build` in the checkout before compiling or running the external consumer. The checkout manifest points to generated `dist/` files; published packages already contain those files.
|
|
108
97
|
|
|
109
|
-
|
|
98
|
+
For an application compiled to ESM with an entry at `dist/index.js`, one possible Bun launch command is:
|
|
110
99
|
|
|
111
100
|
```sh
|
|
112
101
|
bun run dist/index.js
|
|
113
102
|
```
|
|
114
103
|
|
|
104
|
+
This is an application launch example, not a repository-tested consumer check.
|
|
105
|
+
|
|
115
106
|
Browser applications use their bundler and the platform Fetch stream and WebSocket capabilities required by the transports they enable.
|
|
116
107
|
|
|
117
108
|
## Documentation
|
|
118
109
|
|
|
119
110
|
Guides live on the documentation site, not in the published tarball.
|
|
120
111
|
|
|
121
|
-
- [Getting started](https://defjs.
|
|
122
|
-
- [
|
|
123
|
-
- [
|
|
124
|
-
- [
|
|
125
|
-
- [
|
|
112
|
+
- [Getting started](https://defjs.lyz.cloud/docs/start)
|
|
113
|
+
- [Build and publish an SDK](https://defjs.lyz.cloud/recipes/publish-http-sdk)
|
|
114
|
+
- [Client](https://defjs.lyz.cloud/docs/client)
|
|
115
|
+
- [HTTP](https://defjs.lyz.cloud/docs/http)
|
|
116
|
+
- [SSE](https://defjs.lyz.cloud/docs/sse)
|
|
117
|
+
- [WebSocket](https://defjs.lyz.cloud/docs/web-socket)
|