lecodes-sdk 2.6.0 → 2.8.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/dist/global.d.ts +7 -7
- package/dist/types/inject.d.ts +1 -1
- package/dist/types/runtime/firstCall.d.ts +12 -0
- package/dist/types/runtime/rpc.d.ts +24 -0
- package/dist/types/runtime/wire.d.ts +11 -4
- package/dist/types/server/channel.d.ts +16 -0
- package/dist/types/server/computed.d.ts +59 -0
- package/dist/types/server/context.d.ts +13 -1
- package/dist/types/server/db/fields.d.ts +2 -1
- package/dist/types/server/db/marci/query.d.ts +53 -6
- package/dist/types/server/inject.d.ts +2 -0
- package/dist/types/server/ordered.d.ts +18 -0
- package/dist/types/version.d.ts +1 -1
- package/dist/types.json +1 -1
- package/package.json +2 -2
- package/src/compile/serverSplit.ts +8 -4
- package/src/compile/serverTypes.ts +17 -3
- package/src/inject.ts +3 -2
- package/src/runtime/firstCall.ts +28 -0
- package/src/runtime/rpc.ts +141 -12
- package/src/runtime/wire.ts +11 -6
- package/src/server/channel.ts +32 -0
- package/src/server/channelHub.ts +246 -16
- package/src/server/clock.ts +50 -0
- package/src/server/computed.ts +84 -0
- package/src/server/computedHost.ts +413 -0
- package/src/server/context.ts +14 -2
- package/src/server/db/defineDb.ts +47 -7
- package/src/server/db/fields.ts +2 -1
- package/src/server/db/httpTransport.ts +37 -2
- package/src/server/db/httpWatch.ts +180 -0
- package/src/server/db/marci/query.ts +112 -7
- package/src/server/db/watch.ts +160 -0
- package/src/server/files/host.ts +2 -1
- package/src/server/host.ts +19 -4
- package/src/server/inject.ts +6 -2
- package/src/server/ordered.ts +41 -0
- package/src/server/rateLimit.ts +72 -0
- package/src/server/runtime.ts +127 -9
- package/src/version.ts +1 -1
package/dist/global.d.ts
CHANGED
|
@@ -127,7 +127,7 @@ declare global {
|
|
|
127
127
|
const SpriteAnimation: typeof SDK.SpriteAnimation
|
|
128
128
|
const SpriteSheet: typeof SDK.SpriteSheet
|
|
129
129
|
const StateMachine: typeof SDK.StateMachine
|
|
130
|
-
const SvgSource: typeof SDK.SvgSource
|
|
130
|
+
const SvgSource: typeof SDK.SvgSource & typeof SERVER.SvgSource
|
|
131
131
|
const System: typeof SDK.System
|
|
132
132
|
const Terrain: typeof SDK.Terrain
|
|
133
133
|
const Text2D: typeof SDK.Text2D
|
|
@@ -181,6 +181,7 @@ declare global {
|
|
|
181
181
|
const __channel: typeof SDK.__channel
|
|
182
182
|
const __compOp: typeof SDK.__compOp
|
|
183
183
|
const __compWrite: typeof SDK.__compWrite
|
|
184
|
+
const __computed: typeof SDK.__computed
|
|
184
185
|
const __localAsset: typeof SDK.__localAsset
|
|
185
186
|
const __pushChannel: typeof SDK.__pushChannel
|
|
186
187
|
const __pushEnable: typeof SDK.__pushEnable
|
|
@@ -195,7 +196,7 @@ declare global {
|
|
|
195
196
|
const cells: typeof SDK.cells
|
|
196
197
|
const clipboard: typeof SDK.clipboard
|
|
197
198
|
const colorCurve: typeof SDK.colorCurve
|
|
198
|
-
const computed: typeof SDK.computed
|
|
199
|
+
const computed: typeof SERVER.computed & typeof SDK.computed
|
|
199
200
|
const cubicBezier: typeof SDK.cubicBezier
|
|
200
201
|
const curve: typeof SDK.curve
|
|
201
202
|
const dataOf: typeof SDK.dataOf
|
|
@@ -213,7 +214,7 @@ declare global {
|
|
|
213
214
|
const easeOut: typeof SDK.easeOut
|
|
214
215
|
const effect: typeof SDK.effect
|
|
215
216
|
const encodeCells: typeof SDK.encodeCells
|
|
216
|
-
const env: typeof SDK.env
|
|
217
|
+
const env: typeof SDK.env & typeof SERVER.env
|
|
217
218
|
const failureOf: typeof SDK.failureOf
|
|
218
219
|
const fetch: typeof SDK.fetch
|
|
219
220
|
const fetchLocal: typeof SDK.fetchLocal
|
|
@@ -223,13 +224,14 @@ declare global {
|
|
|
223
224
|
const localStorage: typeof SDK.localStorage
|
|
224
225
|
const make: typeof SDK.make
|
|
225
226
|
const mediaDevices: typeof SDK.mediaDevices
|
|
227
|
+
const online: typeof SDK.online
|
|
226
228
|
const openFilePicker: typeof SDK.openFilePicker
|
|
227
229
|
const openURL: typeof SDK.openURL
|
|
228
230
|
const packTile: typeof SDK.packTile
|
|
229
231
|
const permissions: typeof SDK.permissions
|
|
230
232
|
const rawCell: typeof SDK.rawCell
|
|
231
233
|
const rawIndex: typeof SDK.rawIndex
|
|
232
|
-
const ref: typeof SDK.ref
|
|
234
|
+
const ref: typeof SDK.ref & typeof SERVER.ref
|
|
233
235
|
const registerEditorTool: typeof SDK.registerEditorTool
|
|
234
236
|
const registerEditorWindow: typeof SDK.registerEditorWindow
|
|
235
237
|
const registerFont: typeof SDK.registerFont
|
|
@@ -724,13 +726,11 @@ declare global {
|
|
|
724
726
|
type Wrap2D = SDK.Wrap2D
|
|
725
727
|
// ---- server globals (available in *.server.ts; using them in client code is a build error) ----
|
|
726
728
|
const ApiError: typeof SERVER.ApiError
|
|
727
|
-
const SvgSource: typeof SERVER.SvgSource
|
|
728
729
|
const channel: typeof SERVER.channel
|
|
729
730
|
const defineDb: typeof SERVER.defineDb
|
|
730
|
-
const env: typeof SERVER.env
|
|
731
731
|
const model: typeof SERVER.model
|
|
732
|
+
const ordered: typeof SERVER.ordered
|
|
732
733
|
const pushChannel: typeof SERVER.pushChannel
|
|
733
|
-
const ref: typeof SERVER.ref
|
|
734
734
|
const request: typeof SERVER.request
|
|
735
735
|
const t: typeof SERVER.t
|
|
736
736
|
type ApiError = SERVER.ApiError
|
package/dist/types/inject.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export { device, type HapticStyle, type MotionOptions } from "./runtime/device";
|
|
|
23
23
|
export { Input, InputChannel, type InputKeyEvent, type InputGamepadEvent, type InputEventName, type GamepadState, type GamepadAxisName } from "./runtime/input";
|
|
24
24
|
export { WebSocket } from "./runtime/net";
|
|
25
25
|
export { Net, NetPlayer, Replicated, NetEntity, type NetMessage, type NetRole, type NetStatus, type NetEvents, type NetLaunch, type NetInput, type NetKind, type NetTransform, type NetCorrection } from "./net";
|
|
26
|
-
export { __rpc, __channel, __pushChannel, __serverOnly, RpcError } from "./runtime/rpc";
|
|
26
|
+
export { __rpc, __channel, __computed, __pushChannel, __serverOnly, RpcError, online } from "./runtime/rpc";
|
|
27
27
|
export { __registerFont } from "./ui/fonts";
|
|
28
28
|
export { resource, action, dataOf, failureOf, type Loaded, type Failure, type Resource, type Action } from "./runtime/resource";
|
|
29
29
|
export type { ChannelSubscription } from "./server/channel";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The client transport's first-call rule (./rpc.ts), kept apart so a host that calls a backend as a
|
|
3
|
+
* device — a flow's `call` step, `lecodes call` (tools/cli/src/dev/backendCall.ts) — follows the very
|
|
4
|
+
* same rule: a device with no token yet sends its first call alone and the calls behind it wait, so a
|
|
5
|
+
* backend that mints a token on first contact (`request.session`, a guest session) mints ONE — two
|
|
6
|
+
* calls in flight would each get their own and the device would end up with whichever landed last.
|
|
7
|
+
* Once per device per run: the gate opens for good when the first call is back, whether or not it
|
|
8
|
+
* brought a token. Pure: no bridges, no globals.
|
|
9
|
+
*/
|
|
10
|
+
export type FirstCallGate = <T>(hasToken: () => boolean, send: () => Promise<T>) => Promise<T>;
|
|
11
|
+
/** One gate per device (and backend): `gate(hasToken, send)` runs `send` under the rule. */
|
|
12
|
+
export declare const firstCallGate: () => FirstCallGate;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* export const listNotes = __rpc("<serverUrl>", "notes.server.ts#listNotes")
|
|
7
7
|
* export const chat = __channel("<serverUrl>", "chat.server.ts#chat")
|
|
8
|
+
* export const roomView = __computed("<serverUrl>", "game.server.ts#roomView")
|
|
8
9
|
* export const db = __serverOnly("schema.server.ts#db")
|
|
9
10
|
*
|
|
10
11
|
* Wire (implemented by the runner):
|
|
@@ -18,7 +19,11 @@
|
|
|
18
19
|
* The session token is transport-owned (cookie-like): stored under `lecodes.session:<serverUrl>` in
|
|
19
20
|
* localStorage, sent on every call and on the socket, replaced whenever a response carries `session`.
|
|
20
21
|
* User code never sees it — "am I logged in" is an endpoint (`me()`), see the plan.
|
|
22
|
+
*
|
|
23
|
+
* `online` (the app's global) is what this transport last learned of its backends: both the calls and
|
|
24
|
+
* the socket go through here, so it is kept here (docs/plans/multiplayer-plan.md D5a).
|
|
21
25
|
*/
|
|
26
|
+
import { type Computed } from "../core/signals";
|
|
22
27
|
import type { ChannelSubscription } from "../server/channel";
|
|
23
28
|
/** Rejection value of a failed endpoint call: `status` mirrors the server's `ApiError`
|
|
24
29
|
* (0 = the request itself failed: no network / no server). */
|
|
@@ -26,6 +31,14 @@ export declare class RpcError extends Error {
|
|
|
26
31
|
readonly status: number;
|
|
27
32
|
constructor(status: number, message: string);
|
|
28
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Does the app reach its backend: `true` until a call fails without reaching it (`RpcError` status 0)
|
|
36
|
+
* or the channel socket drops and starts reconnecting; `true` again once the socket opens, a message
|
|
37
|
+
* comes over it, or a call is answered — with an error too. With several backends, all of them. Read
|
|
38
|
+
* `.value` in a binding, a `computed` or an `effect`; it is not the device's network (`device.online`)
|
|
39
|
+
* and says nothing of whether what a screen shows is old: a computed re-delivers by itself.
|
|
40
|
+
*/
|
|
41
|
+
export declare const online: Computed<boolean>;
|
|
29
42
|
/** Build the client proxy of endpoint `id`. Positional args travel as a JSON array. */
|
|
30
43
|
export declare const __rpc: (serverUrl: string, id: string) => (...args: unknown[]) => Promise<any>;
|
|
31
44
|
/** A server-only export (db, models, helpers) referenced from client code: any use throws. */
|
|
@@ -38,6 +51,17 @@ export declare const __serverOnly: (id: string) => any;
|
|
|
38
51
|
export declare const __channel: (serverUrl: string, id: string) => {
|
|
39
52
|
subscribe(...all: unknown[]): ChannelSubscription;
|
|
40
53
|
publish(): never;
|
|
54
|
+
members(): never;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Build the client proxy of computed `id` — what a `computed()` export of a `*.server.ts` file is in the
|
|
58
|
+
* app (its type is the server's: src/server/computed.ts). Called, it is an endpoint call;
|
|
59
|
+
* `subscribe([...args,] handler, { error? })` hears its value now (the `ok`) and on every change (an `ev`
|
|
60
|
+
* to the subscription's own group), over the channel socket.
|
|
61
|
+
*/
|
|
62
|
+
export declare const __computed: (serverUrl: string, id: string) => ((...args: unknown[]) => Promise<any>) & {
|
|
63
|
+
subscribe(...all: unknown[]): ChannelSubscription;
|
|
64
|
+
watchers(): never;
|
|
41
65
|
};
|
|
42
66
|
/**
|
|
43
67
|
* Build the client proxy of push channel `id` — what a `pushChannel()` export of a `*.server.ts` file is
|
|
@@ -7,15 +7,20 @@
|
|
|
7
7
|
* then DROPS the socket's subscriptions (they were granted to someone else) and the
|
|
8
8
|
* client sends them again, under new ids.
|
|
9
9
|
* → sub subscribe to channel `ch`; `args` are what the app passed to `subscribe` before the
|
|
10
|
-
* handler — the channel's `groupBy` reads them.
|
|
10
|
+
* handler — the channel's `groupBy` reads them. With `computed: true`, to the computed
|
|
11
|
+
* `ch` (../server/computed.ts): `args` are its arguments.
|
|
11
12
|
* → unsub
|
|
12
13
|
* ← session a token minted while the socket was served (the first contact of a guest): the client
|
|
13
14
|
* stores it; nothing is dropped, the server already has it.
|
|
14
15
|
* ← ok the subscription stands; `group` is where the server put it (null = a channel without
|
|
15
|
-
* groups). The client learns its group only here — it never names one.
|
|
16
|
-
*
|
|
16
|
+
* groups). The client learns its group only here — it never names one. A computed's
|
|
17
|
+
* group is a number the server made for that one subscription, and `data` its value.
|
|
18
|
+
* ← err refused (a hook threw) — the subscription is gone. For a computed: refused when it comes
|
|
19
|
+
* before the `ok` (the first run threw); after it, a later run failed and the
|
|
20
|
+
* subscription stands.
|
|
17
21
|
* ← ev one published message, addressed by channel + group so the server sends one frame to
|
|
18
|
-
* every socket of a group; the client hands it to each of its subscriptions there.
|
|
22
|
+
* every socket of a group; the client hands it to each of its subscriptions there. For a
|
|
23
|
+
* computed: its next value, sent only when it differs from the last one.
|
|
19
24
|
*
|
|
20
25
|
* A group's value is not a secret: its subscriber is told it.
|
|
21
26
|
*/
|
|
@@ -29,6 +34,7 @@ export type ClientFrame = {
|
|
|
29
34
|
id: number;
|
|
30
35
|
ch: string;
|
|
31
36
|
args: unknown[];
|
|
37
|
+
computed?: true;
|
|
32
38
|
} | {
|
|
33
39
|
t: "unsub";
|
|
34
40
|
id: number;
|
|
@@ -40,6 +46,7 @@ export type ServerFrame = {
|
|
|
40
46
|
t: "ok";
|
|
41
47
|
id: number;
|
|
42
48
|
group: ChannelGroup | null;
|
|
49
|
+
data?: unknown;
|
|
43
50
|
} | {
|
|
44
51
|
t: "err";
|
|
45
52
|
id: number;
|
|
@@ -22,6 +22,11 @@
|
|
|
22
22
|
* `globalThis.__lecodesPublish` seam the host installs (same reason as ./context.ts: the bundle
|
|
23
23
|
* carries its own SDK copy). In the app the export is another object altogether — the proxy
|
|
24
24
|
* `__channel` of src/runtime/rpc.ts — which is why `subscribe` here only throws.
|
|
25
|
+
*
|
|
26
|
+
* Presence: `roomChannel.members(code)` (and a computed's `watchers(...args)`, ./computed.ts) — the
|
|
27
|
+
* sessions subscribed to a group now. The hub (./channelHub.ts) holds that table; the read goes to it
|
|
28
|
+
* through the `globalThis.__lecodesPresence` seam the host installs (./computedHost.ts
|
|
29
|
+
* `setPresenceProvider`), keyed by the export and the subscription's key as JSON (`presenceKey`).
|
|
25
30
|
*/
|
|
26
31
|
import type { ChannelGroup } from "../runtime/wire";
|
|
27
32
|
export type { ChannelGroup };
|
|
@@ -48,6 +53,9 @@ export interface Channel<M> {
|
|
|
48
53
|
groupBy<K extends ChannelGroup, A extends unknown[] = []>(group: (...args: A) => K | Promise<K>): GroupedChannel<M, K, A>;
|
|
49
54
|
/** Server: send `message` to every subscriber (at most once — nothing is kept for a socket that is away). */
|
|
50
55
|
publish(message: M): void;
|
|
56
|
+
/** Server: the sessions (`request.session`) subscribed now, each once. Inside a `computed`, a join or a
|
|
57
|
+
* leave runs it again. */
|
|
58
|
+
members(): Promise<number[]>;
|
|
51
59
|
/** App: listen until `close()`. */
|
|
52
60
|
subscribe(handler: (message: M) => void, options?: SubscribeOptions): ChannelSubscription;
|
|
53
61
|
}
|
|
@@ -55,12 +63,20 @@ export interface Channel<M> {
|
|
|
55
63
|
export interface GroupedChannel<M, K extends ChannelGroup, A extends unknown[]> {
|
|
56
64
|
/** Server: send `message` to the subscribers of `group`. */
|
|
57
65
|
publish(group: K, message: M): void;
|
|
66
|
+
/** Server: the sessions (`request.session`) subscribed to `group` now, each once. Inside a `computed`,
|
|
67
|
+
* a join or a leave runs it again. */
|
|
68
|
+
members(group: K): Promise<number[]>;
|
|
58
69
|
/** App: listen until `close()`; the arguments before the handler go to the channel's `groupBy`. */
|
|
59
70
|
subscribe(...args: [...args: A, handler: (message: M) => void, options?: SubscribeOptions]): ChannelSubscription;
|
|
60
71
|
}
|
|
61
72
|
type Publisher = (channelId: string, group: ChannelGroup | null, message: unknown) => void;
|
|
62
73
|
/** Host hook (runner / local backend / test harness): where `publish` delivers to. */
|
|
63
74
|
export declare const setChannelPublisher: (publisher: Publisher | null) => void;
|
|
75
|
+
/** What a presence read asks the host: the sessions subscribed to export `id` under `key` (`presenceKey`). */
|
|
76
|
+
export type PresenceAsk = (id: string, key: string) => Promise<number[]>;
|
|
77
|
+
/** The key a subscription is present under, the hub's and the reads' alike: a computed's arguments, a
|
|
78
|
+
* channel's group (null without groups) — as JSON. */
|
|
79
|
+
export declare const presenceKey: (what: unknown) => string;
|
|
64
80
|
export declare const isChannelGroup: (v: unknown) => v is ChannelGroup;
|
|
65
81
|
export declare const channel: <M = unknown>() => Channel<M>;
|
|
66
82
|
export declare const isChannel: (v: unknown) => v is ChannelRecord;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `computed(fn)` — a server function the runtime keeps fresh (server global). The app calls it once,
|
|
3
|
+
* like an endpoint, or subscribes to it: the value now, and again whenever the data it read changes.
|
|
4
|
+
*
|
|
5
|
+
* export const roomView = computed(async (code: string) => { // game.server.ts
|
|
6
|
+
* const room = await db.room.where({ code }).first()
|
|
7
|
+
* const players = await db.player.where({ room: code }).select({ name: true, device: true })
|
|
8
|
+
* return { room, players, me: request.session }
|
|
9
|
+
* })
|
|
10
|
+
*
|
|
11
|
+
* const sub = roomView.subscribe(code, (view) => { room.value = view }) // app: now, then on every change
|
|
12
|
+
* sub.close()
|
|
13
|
+
* const view = await roomView(code) // app: one call
|
|
14
|
+
*
|
|
15
|
+
* The app's `computed` (src/core/signals.ts) is the same idea: a value derived from its inputs, made
|
|
16
|
+
* again when they change. The app's inputs are signals; the server's are the database's rows. Hence its
|
|
17
|
+
* two rules: it only READS (a write inside is an error), and its value follows from its data and its
|
|
18
|
+
* arguments alone (`Math.random()` inside = a value that is new at every run).
|
|
19
|
+
*
|
|
20
|
+
* How it stays fresh (./computedHost.ts): a subscription runs `fn` in the subscriber's request scope
|
|
21
|
+
* (`request.session`, `db.auth`), and every read the db client sends meanwhile becomes a WATCH of the
|
|
22
|
+
* database (./db/watch.ts). When the database says a watched result may have changed, `fn` runs again —
|
|
23
|
+
* once for any number of signals during a run — and the subscriber hears the value only when it differs
|
|
24
|
+
* from the last one (JSON equality). The subscription rides the channel socket (a `sub` frame with
|
|
25
|
+
* `computed: true`, ../runtime/wire.ts); the hub (./channelHub.ts) holds it across a new worker (a deploy,
|
|
26
|
+
* an eviction) and runs it there again.
|
|
27
|
+
*
|
|
28
|
+
* Identity, as a channel's: the export (`<path>#<name>`), set by the runtime when the bundle is loaded
|
|
29
|
+
* (`loadServerModules`). In the bundle the export is callable — another server function may
|
|
30
|
+
* `await roomView(code)`, its reads are then that function's own — and `subscribe` only throws; in the
|
|
31
|
+
* app it is the proxy `__computed` of src/runtime/rpc.ts.
|
|
32
|
+
*
|
|
33
|
+
* Who is here: `roomView.watchers(code)` — the sessions subscribed to it with these arguments now (the
|
|
34
|
+
* hub's table, read as a channel's `members` is: ./channel.ts `askPresence`). A read, not a hook: inside
|
|
35
|
+
* a computed it is one more input, and a join or a leave there runs that computed again (./computedHost.ts).
|
|
36
|
+
*/
|
|
37
|
+
import { type ChannelSubscription } from "./channel";
|
|
38
|
+
export type ComputedOptions = {
|
|
39
|
+
/** The server refused the subscription (its first run threw), or a later run failed: an `RpcError`,
|
|
40
|
+
* its `status` the thrown one. Without it the failure is logged. A refused subscription is asked
|
|
41
|
+
* again when the session changes; after a failed later run the last value stands and the next change
|
|
42
|
+
* runs it again. */
|
|
43
|
+
error?: (e: Error & {
|
|
44
|
+
readonly status: number;
|
|
45
|
+
}) => void;
|
|
46
|
+
};
|
|
47
|
+
/** A server function the runtime keeps fresh: `computed(async (…args) => value)`. */
|
|
48
|
+
export interface ServerComputed<A extends unknown[], R> {
|
|
49
|
+
/** One call, as an endpoint's. */
|
|
50
|
+
(...args: A): Promise<R>;
|
|
51
|
+
/** App: `handler` gets the value now, and again each time it changes, until `close()`; the arguments
|
|
52
|
+
* before it are the function's. */
|
|
53
|
+
subscribe(...args: [...args: A, handler: (value: R) => void, options?: ComputedOptions]): ChannelSubscription;
|
|
54
|
+
/** Server: the sessions (`request.session`) subscribed to it with these arguments now, each once — the
|
|
55
|
+
* arguments as the app passed them, matched as JSON. Inside a `computed`, a join or a leave runs it again. */
|
|
56
|
+
watchers(...args: A): Promise<number[]>;
|
|
57
|
+
}
|
|
58
|
+
export declare const computed: <A extends unknown[], R>(fn: (...args: A) => Promise<R>) => ServerComputed<A, R>;
|
|
59
|
+
export declare const isComputed: (v: unknown) => v is ComputedRecord;
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
* server bundle carries its own copy of this module (chisel bundles the SDK in), so the host can't
|
|
5
5
|
* hand it a value by import. `request` reads through that seam; outside a request (module load,
|
|
6
6
|
* timers after the response) every member is undefined.
|
|
7
|
+
*
|
|
8
|
+
* `session` is decided in ONE place, the host runtime (./runtime.ts `defineSession`), for endpoints
|
|
9
|
+
* and channel hooks alike: every host (the runner, `lecodes dev`, a test) gets it by running calls
|
|
10
|
+
* through `createInvoker` / `createSubscribeGuard`.
|
|
7
11
|
*/
|
|
8
12
|
export type RequestContext = {
|
|
9
13
|
/** Endpoint id `<path>#<export>` (or the channel id for a subscription's hooks). */
|
|
@@ -14,12 +18,17 @@ export type RequestContext = {
|
|
|
14
18
|
sessionToken?: string;
|
|
15
19
|
/** Set by the auth runtime after resolving the token — read via `db.auth`, not here. */
|
|
16
20
|
auth?: unknown;
|
|
21
|
+
/** The device of the request — `request.session`. Defined by the host runtime (./runtime.ts), read lazily:
|
|
22
|
+
* a host never sets it. */
|
|
23
|
+
readonly session?: number;
|
|
17
24
|
/** The files a multipart call carried, as the receiving process left them (./files/host.ts `Upload`). */
|
|
18
25
|
uploads?: unknown[];
|
|
19
26
|
/** What `/files/<id>/<name>` is appended to in a stored file's url: the backend's own base, as this request reached it. */
|
|
20
27
|
filesBase?: string;
|
|
21
28
|
};
|
|
22
|
-
|
|
29
|
+
/** `quiet`: the host's own read, not the code's (a stored file's url is made of the request's `filesBase`).
|
|
30
|
+
* Every other read is the code's: inside a computed it makes the run its subscriber's alone (./computedHost.ts). */
|
|
31
|
+
type Provider = (quiet?: boolean) => RequestContext | undefined;
|
|
23
32
|
/** Host hook (runner / test harness): install the current-request provider. */
|
|
24
33
|
export declare const setRequestProvider: (provider: Provider | null) => void;
|
|
25
34
|
/** The current request (server global). Undefined members outside a request. */
|
|
@@ -27,5 +36,8 @@ export declare const request: {
|
|
|
27
36
|
readonly id: string | undefined;
|
|
28
37
|
readonly headers: Record<string, string>;
|
|
29
38
|
readonly ip: string | undefined;
|
|
39
|
+
/** The calling device: a number that stays the same for as long as the app keeps its storage, without
|
|
40
|
+
* sign-in; with `withAuth` it is `db.auth.sessionId`. Not a secret — the token behind it is. */
|
|
41
|
+
readonly session: number | undefined;
|
|
30
42
|
};
|
|
31
43
|
export {};
|
|
@@ -80,7 +80,8 @@ export interface Field<T, M extends FieldMeta = FieldMeta> {
|
|
|
80
80
|
unique(group?: string): Field<T, M>;
|
|
81
81
|
/** `@index` — used by `$where`, `$order`, aggregations. */
|
|
82
82
|
index(): Field<T, M>;
|
|
83
|
-
/** `@default(...)`; `"now"` on `t.date()` → `now()
|
|
83
|
+
/** `@default(...)`; `"now"` on `t.date()` → `now()`, a number is milliseconds since 1970; a json's is
|
|
84
|
+
* any JSON value (`t.json<number[]>().default([])`). Makes the field optional on insert. */
|
|
84
85
|
default(value: DefaultOf<T, M>): Field<T, SetMeta<M, "hasDefault", true>>;
|
|
85
86
|
/** Primitive list (`String[]`). */
|
|
86
87
|
array(): Field<T, SetMeta<M, "array", true>>;
|
|
@@ -197,6 +197,39 @@ export type Settles<T> = PromiseLike<T> & {
|
|
|
197
197
|
export type Op<T> = Settles<T> & {
|
|
198
198
|
readonly [__op]: T;
|
|
199
199
|
};
|
|
200
|
+
/** A running watch — see `ReadOp.watch`. */
|
|
201
|
+
export interface Watch {
|
|
202
|
+
/** Stops it: no callback after this returns. Idempotent. */
|
|
203
|
+
close(): void;
|
|
204
|
+
}
|
|
205
|
+
export type WatchOptions = {
|
|
206
|
+
/** Gets what a read of the watch failed with (an op the database refuses — after a migration, say),
|
|
207
|
+
* and what the callback threw. Without it, they are logged. */
|
|
208
|
+
error?: (error: unknown) => void;
|
|
209
|
+
};
|
|
210
|
+
/** A read (`select`, `first`, `count`, `aggregate`): an op that can also be watched. */
|
|
211
|
+
export type ReadOp<T> = Op<T> & {
|
|
212
|
+
/**
|
|
213
|
+
* Calls `callback` with the result at once, then again each time a commit changed it. The database signals
|
|
214
|
+
* every commit that may have changed it; the client reads again and calls back only when the result
|
|
215
|
+
* differs (as JSON) from the one it delivered last. Signals that arrive during a read make one more read.
|
|
216
|
+
* A dropped connection is reopened and read again by the client — nothing to handle outside.
|
|
217
|
+
*/
|
|
218
|
+
watch(callback: (result: T) => void, options?: WatchOptions): Watch;
|
|
219
|
+
};
|
|
220
|
+
/** What a transport tells a watch it holds (internal: `op.watch` is built on it). */
|
|
221
|
+
export type WatchListener = {
|
|
222
|
+
/** A commit may have changed the result — or the transport recovered a lost connection. */
|
|
223
|
+
changed(): void;
|
|
224
|
+
/** The database refused the op when the transport registered it again. */
|
|
225
|
+
failed(error: unknown): void;
|
|
226
|
+
};
|
|
227
|
+
/** A watch registered with a transport: `ready` settles once the database has it (it rejects with the
|
|
228
|
+
* refusal), `close` drops it. */
|
|
229
|
+
export type WatchRegistration = {
|
|
230
|
+
ready: Promise<void>;
|
|
231
|
+
close(): void;
|
|
232
|
+
};
|
|
200
233
|
export type MarciOp = {
|
|
201
234
|
model: string;
|
|
202
235
|
action: string;
|
|
@@ -218,6 +251,16 @@ export type JournalArgs = {
|
|
|
218
251
|
export type MarciTransport = {
|
|
219
252
|
exec(op: MarciOp): Promise<any>;
|
|
220
253
|
batch(ops: MarciOp[]): Promise<any[]>;
|
|
254
|
+
/** The binary read fast path, if the transport has one. `schemaHash` is the fingerprint of the schema the
|
|
255
|
+
* calling client was generated from; the transport answers with the binary result buffer only for a
|
|
256
|
+
* database on the same schema. `{ json }` is an already-parsed JSON answer (HTTP, when the server declined
|
|
257
|
+
* binary); `null` means "no binary answer" and the client falls back to `exec`. */
|
|
258
|
+
queryBinary?(op: MarciOp, schemaHash: string): Promise<Uint8Array | {
|
|
259
|
+
json: any;
|
|
260
|
+
} | null>;
|
|
261
|
+
/** Watches, if the transport has them: registers the read `op` and tells `listener` whenever a commit may
|
|
262
|
+
* have changed its result. Only signals — the re-read and the comparison are the query layer's. */
|
|
263
|
+
watch?(op: MarciOp, listener: WatchListener): WatchRegistration;
|
|
221
264
|
};
|
|
222
265
|
export type ModelTypes = {
|
|
223
266
|
name: string;
|
|
@@ -259,6 +302,8 @@ export type IdArg<I> = I | BareId<I>;
|
|
|
259
302
|
*/
|
|
260
303
|
export interface Query<T extends ModelTypes, Sel = T["scalars"]> extends Settles<Rows<T, Sel>[]> {
|
|
261
304
|
readonly [__op]: Rows<T, Sel>[];
|
|
305
|
+
/** The rows now, then each time a commit changed them — see `ReadOp.watch`. */
|
|
306
|
+
watch(callback: (rows: Rows<T, Sel>[]) => void, options?: WatchOptions): Watch;
|
|
262
307
|
readonly [__sub]: T["name"];
|
|
263
308
|
readonly [__sel]: Effective<T, Sel>;
|
|
264
309
|
/** Filter (marcidb `$where`). Repeated calls are ANDed. */
|
|
@@ -279,18 +324,18 @@ export interface Query<T extends ModelTypes, Sel = T["scalars"]> extends Settles
|
|
|
279
324
|
*/
|
|
280
325
|
select<S extends T["query"] = T["scalars"]>(shape?: S): Query<T, S>;
|
|
281
326
|
/** The first matching row or `null`. */
|
|
282
|
-
first():
|
|
327
|
+
first(): ReadOp<Rows<T, Sel> | null>;
|
|
283
328
|
/** Row count; inside a select it becomes `{ count }` for the relation. */
|
|
284
|
-
count():
|
|
329
|
+
count(): ReadOp<number> & Sub<T["name"], {
|
|
285
330
|
$count: true;
|
|
286
331
|
}>;
|
|
287
332
|
/** @deprecated pass the filter through the chain: `db.user.where(w).count()` (removed in the next minor). */
|
|
288
333
|
count(query: {
|
|
289
334
|
$where?: T["where"];
|
|
290
|
-
}):
|
|
335
|
+
}): ReadOp<number> & Sub<T["name"], {
|
|
291
336
|
$count: true;
|
|
292
337
|
}>;
|
|
293
|
-
aggregate<A extends T["aggregate"]>(query: A):
|
|
338
|
+
aggregate<A extends T["aggregate"]>(query: A): ReadOp<AggregateResult<T["model"], A>> & Sub<T["name"], NoInfer<A>>;
|
|
294
339
|
insert(data: T["insert"]): Op<T["id"]>;
|
|
295
340
|
/** The new row by `select`'s shape — its defaults, dates and files as stored: one call, nothing to read back. */
|
|
296
341
|
insert<S extends T["query"]>(data: T["insert"], options: {
|
|
@@ -322,9 +367,9 @@ export interface Query<T extends ModelTypes, Sel = T["scalars"]> extends Settles
|
|
|
322
367
|
*/
|
|
323
368
|
deleteMany(): Op<number>;
|
|
324
369
|
/** @deprecated use `.select(query)` — it takes the same object, merged with the chain (removed in the next minor). */
|
|
325
|
-
findMany<Q extends T["query"] = {}>(query?: Q):
|
|
370
|
+
findMany<Q extends T["query"] = {}>(query?: Q): ReadOp<Rows<T, Q>[]>;
|
|
326
371
|
/** @deprecated use `.select(query).first()` (removed in the next minor). */
|
|
327
|
-
findFirst<Q extends T["query"] = {}>(query?: Q):
|
|
372
|
+
findFirst<Q extends T["query"] = {}>(query?: Q): ReadOp<Rows<T, Q> | null>;
|
|
328
373
|
}
|
|
329
374
|
/** What a journal can record. Only deletes so far. */
|
|
330
375
|
export type JournalOp = "delete";
|
|
@@ -379,6 +424,8 @@ export type QueryLayerOptions = {
|
|
|
379
424
|
/** Hook for the `onInsert` part of an `upsert` — the fields used only when the row is created (the lecodes SDK
|
|
380
425
|
* puts a uuid id here). Receives what the caller gave (or undefined), returns what is sent. */
|
|
381
426
|
prepareUpsert?(model: string, onInsert: any): any;
|
|
427
|
+
/** The transport's watches (`MarciTransport.watch`); without it `op.watch` throws. */
|
|
428
|
+
watch?(op: MarciOp, listener: WatchListener): WatchRegistration;
|
|
382
429
|
};
|
|
383
430
|
/**
|
|
384
431
|
* The query builder over a set of models and a `run` function. Returns `op(descriptor)` — a lazy operation
|
|
@@ -2,8 +2,10 @@ export { t, model, defineDb, ref } from "./db";
|
|
|
2
2
|
export { ApiError } from "./errors";
|
|
3
3
|
export { env, type Env } from "./env";
|
|
4
4
|
export { channel } from "./channel";
|
|
5
|
+
export { computed } from "./computed";
|
|
5
6
|
export { pushChannel, type PushChannel, type PushMessage, type PushNotification, type PushTarget } from "./pushChannel";
|
|
6
7
|
export { request } from "./context";
|
|
8
|
+
export { ordered } from "./ordered";
|
|
7
9
|
export type { StoredFile, StoredImage } from "./files/models";
|
|
8
10
|
/** The value an SVG source is to the client (`assetIcon(…)` compiles to `SvgSource(…)`): a shared module of
|
|
9
11
|
* the design that names an icon per row loads on the server too. The server draws nothing with it. */
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ordered(key, fn)` — calls with one key run one after another (server global). A worker runs its
|
|
3
|
+
* calls concurrently and every `await` lets another one in, so a read-check-write under a key (a
|
|
4
|
+
* room's code) needs the key's other calls to wait: a chain of promises per key, in this worker. A
|
|
5
|
+
* key is dropped when its chain drains; different keys run in parallel; nothing else is kept (the
|
|
6
|
+
* state stays in the database). Honest because one worker serves a project at a time — the runner
|
|
7
|
+
* lets the new worker of a deploy take calls only once the old one's are done (tools/runner/src/project.ts).
|
|
8
|
+
*
|
|
9
|
+
* Two rules: keep the body short (the key's other callers wait; a call has 30 s), and no `ordered` of a
|
|
10
|
+
* key from inside its own body (it would wait for itself) — that rejects. The keys a body holds follow
|
|
11
|
+
* its async chain in an AsyncLocalStorage: a body's sibling (another call of the same request) is not
|
|
12
|
+
* taken for re-entry, nor is a timer that fires after the body ended. The class comes over a global seam
|
|
13
|
+
* the host runtime installs (./runtime.ts): the bundle carries its own copy of this module and imports
|
|
14
|
+
* nothing. Without the seam the queue still works; only re-entry is not caught.
|
|
15
|
+
*/
|
|
16
|
+
export type OrderedKey = string | number;
|
|
17
|
+
/** Run `fn` once every earlier call of `key` has finished; its result (or failure) is the call's. */
|
|
18
|
+
export declare const ordered: <T>(key: OrderedKey, fn: () => T | Promise<T>) => Promise<T>;
|
package/dist/types/version.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "2.
|
|
1
|
+
export declare const SDK_VERSION = "2.8.0";
|
|
2
2
|
/** The major of a semver string, or null when it is not one. */
|
|
3
3
|
export declare const sdkMajor: (version: string | null | undefined) => number | null;
|
|
4
4
|
/** The `// sdk: <version>` line of a compiled bundle's header (sdk/src/compile/header.ts), read from
|