lecodes-sdk 2.6.0 → 2.8.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/dist/global.d.ts +11 -7
- package/dist/types/inject.d.ts +2 -1
- package/dist/types/plugin.d.ts +1 -0
- package/dist/types/plugins/gen/ar/sdk/ar.d.ts +25 -5
- package/dist/types/runtime/firstCall.d.ts +12 -0
- package/dist/types/runtime/gestures.d.ts +52 -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/ui/UIModal.d.ts +5 -3
- package/dist/types/version.d.ts +1 -1
- package/dist/types.json +1 -1
- package/package.json +2 -2
- package/src/bridges/tree.d.ts +14 -4
- package/src/compile/serverSplit.ts +8 -4
- package/src/compile/serverTypes.ts +17 -3
- package/src/inject.ts +5 -2
- package/src/plugin.ts +3 -1
- package/src/plugins/gen/ar/sdk/ar.ts +59 -14
- package/src/runtime/firstCall.ts +28 -0
- package/src/runtime/gestures.ts +190 -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/ui/UIModal.ts +44 -9
- package/src/ui/UINode.ts +4 -0
- package/src/ui/tree.ts +8 -0
- 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
|
|
@@ -246,6 +248,7 @@ declare global {
|
|
|
246
248
|
const tileOriented: typeof SDK.tileOriented
|
|
247
249
|
const tileTurns: typeof SDK.tileTurns
|
|
248
250
|
const toast: typeof SDK.toast
|
|
251
|
+
const trackGesture: typeof SDK.trackGesture
|
|
249
252
|
const use: typeof SDK.use
|
|
250
253
|
type ARError = SDK.ARError
|
|
251
254
|
type ARErrorCode = SDK.ARErrorCode
|
|
@@ -376,6 +379,9 @@ declare global {
|
|
|
376
379
|
type GeoPosition = SDK.GeoPosition
|
|
377
380
|
type GeoWatch = SDK.GeoWatch
|
|
378
381
|
type Geometry = SDK.Geometry
|
|
382
|
+
type Gesture = SDK.Gesture
|
|
383
|
+
type GestureHandler = SDK.GestureHandler
|
|
384
|
+
type GestureTarget = SDK.GestureTarget
|
|
379
385
|
type GizmoAnchor = SDK.GizmoAnchor
|
|
380
386
|
type GizmoStyle = SDK.GizmoStyle
|
|
381
387
|
type HapticStyle = SDK.HapticStyle
|
|
@@ -724,13 +730,11 @@ declare global {
|
|
|
724
730
|
type Wrap2D = SDK.Wrap2D
|
|
725
731
|
// ---- server globals (available in *.server.ts; using them in client code is a build error) ----
|
|
726
732
|
const ApiError: typeof SERVER.ApiError
|
|
727
|
-
const SvgSource: typeof SERVER.SvgSource
|
|
728
733
|
const channel: typeof SERVER.channel
|
|
729
734
|
const defineDb: typeof SERVER.defineDb
|
|
730
|
-
const env: typeof SERVER.env
|
|
731
735
|
const model: typeof SERVER.model
|
|
736
|
+
const ordered: typeof SERVER.ordered
|
|
732
737
|
const pushChannel: typeof SERVER.pushChannel
|
|
733
|
-
const ref: typeof SERVER.ref
|
|
734
738
|
const request: typeof SERVER.request
|
|
735
739
|
const t: typeof SERVER.t
|
|
736
740
|
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";
|
|
@@ -37,6 +37,7 @@ export { audio, type PlaySoundOptions, type SpatialOptions, type Rolloff, type A
|
|
|
37
37
|
export { openFilePicker, files, type FileData } from "./runtime/files";
|
|
38
38
|
export { share } from "./runtime/share";
|
|
39
39
|
export { ClickEvent, TouchStartEvent, LongPressEvent, MouseEnterEvent } from "./runtime/touch";
|
|
40
|
+
export { trackGesture, type Gesture, type GestureHandler, type GestureTarget } from "./runtime/gestures";
|
|
40
41
|
export { Scene2D } from "./g2/Scene2D";
|
|
41
42
|
export { Node2D } from "./g2/Node2D";
|
|
42
43
|
export { Camera2D } from "./g2/Camera2D";
|
package/dist/types/plugin.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ export { Vec2, Vec3, type Vec3Like } from "./math/vec";
|
|
|
22
22
|
export { Quat, type QuatLike } from "./math/quat";
|
|
23
23
|
export { Mat4 } from "./math/mat4";
|
|
24
24
|
export type { ClickEvent, TouchStartEvent } from "./runtime/touch";
|
|
25
|
+
export { trackGesture, type Gesture, type GestureHandler, type GestureTarget } from "./runtime/gestures";
|
|
25
26
|
export type { DismissOptions } from "./ui/presentable";
|
|
26
27
|
/** The seam the push plugin's wrapper installs its runtime on (src/runtime/pushSeam.ts): what the app's transport
|
|
27
28
|
* reads the device's address from, registers through, and a push channel's handlers are kept by. */
|
|
@@ -44,6 +44,9 @@ export type ARSceneOptions = SceneOptions & {
|
|
|
44
44
|
/** "world" (default): planes, hit tests, anchors, image and QR targets. "images": image and QR
|
|
45
45
|
* targets only — lighter on iOS; no planes, no hit test, no `root`. */
|
|
46
46
|
mode?: ARMode;
|
|
47
|
+
/** How far in front of the user `root` is placed, metres (default 1): along the camera's heading,
|
|
48
|
+
* on the floor. */
|
|
49
|
+
rootDistance?: number;
|
|
47
50
|
};
|
|
48
51
|
export type AROpenOptions = PresentOptions & Omit<StartOptions, "mode" | "images" | "codes"> & {
|
|
49
52
|
/** The scene's ambient (its `environmentIntensity`) follows the estimated light; turns
|
|
@@ -128,6 +131,8 @@ export declare class ARScene extends Scene {
|
|
|
128
131
|
/** Whether this host registered the "ar" service (the app lists the plugin, the host has a half). */
|
|
129
132
|
static get isSupported(): boolean;
|
|
130
133
|
readonly mode: ARMode;
|
|
134
|
+
/** Metres between the user and `root` when it is placed (`rootDistance` option, default 1). */
|
|
135
|
+
readonly rootDistance: number;
|
|
131
136
|
/** `open()` warm-renders the scene first (default true). */
|
|
132
137
|
useWarmRender: boolean;
|
|
133
138
|
private _opening;
|
|
@@ -147,7 +152,8 @@ export declare class ARScene extends Scene {
|
|
|
147
152
|
private _codesWarned;
|
|
148
153
|
private _options;
|
|
149
154
|
private _root;
|
|
150
|
-
/** The first horizontal plane
|
|
155
|
+
/** The floor's height (world Y, metres): the first horizontal plane below the camera this session
|
|
156
|
+
* found. Null until one is; `root` is placed on it. */
|
|
151
157
|
private _floor;
|
|
152
158
|
/** The image targets. `source` is held for the bytes: a fetched response owns its buffer, which the
|
|
153
159
|
* host frees once the response is collected — the File the session is handed only names it. */
|
|
@@ -174,9 +180,17 @@ export declare class ARScene extends Scene {
|
|
|
174
180
|
/** A grid drawn on every plane (each plane's child mesh). */
|
|
175
181
|
get showPlanes(): boolean;
|
|
176
182
|
set showPlanes(on: boolean);
|
|
177
|
-
/** The
|
|
178
|
-
*
|
|
183
|
+
/** The place in front of the user: an anchor `rootDistance` metres ahead of the camera along its
|
|
184
|
+
* heading, on the floor — the first horizontal plane below the camera the session finds; it is
|
|
185
|
+
* pinned there and fires `track` then — turned the way the user looks (its −Z along the heading,
|
|
186
|
+
* so a model's front, +Z, faces the user). Until the floor is found (and on the web, which finds
|
|
187
|
+
* none) it sits the same way at the camera's own height, not pinned. Made on first use; put
|
|
188
|
+
* content under it. `placeRoot()` puts it in front of the user again. Not in "images" mode. */
|
|
179
189
|
get root(): Anchor;
|
|
190
|
+
/** Put `root` in front of the user again — where they look now, `rootDistance` metres ahead, on the
|
|
191
|
+
* floor if one is known (the camera's height otherwise), facing them: a "reset" button, or a
|
|
192
|
+
* placement after the user has moved. Makes `root` when there is none yet. */
|
|
193
|
+
placeRoot(): Anchor;
|
|
180
194
|
/** An anchor that follows an image (a printed marker, a poster): the session finds it in the camera
|
|
181
195
|
* image and keeps the anchor on it — a rigid pose in metres (its centre, Y out of the image, the
|
|
182
196
|
* image in XZ), its size `anchor.extent`. `physicalWidth` = the printed width in metres. Hidden
|
|
@@ -244,8 +258,14 @@ export declare class ARScene extends Scene {
|
|
|
244
258
|
private _wire;
|
|
245
259
|
/** Pin `anchor` at `pose` (where it is until the session holds it) — now, or when a session starts. */
|
|
246
260
|
private _place;
|
|
247
|
-
/** `root`
|
|
248
|
-
|
|
261
|
+
/** Where `root` goes now: `rootDistance` metres from the camera along its heading (its forward
|
|
262
|
+
* flattened to the floor; looking straight down or up, the way the top of the screen points), at
|
|
263
|
+
* the floor's height when one is known, else the camera's; turned so its −Z runs along the
|
|
264
|
+
* heading — a model's front (+Z) faces the user. */
|
|
265
|
+
private _rootPose;
|
|
266
|
+
/** `root` in front of the user: moved there now, and pinned there when the floor is known (a
|
|
267
|
+
* session holds a pin; without one `_pinAll` does not re-pin root — it waits for its own floor). */
|
|
268
|
+
private _placeRoot;
|
|
249
269
|
private _pin;
|
|
250
270
|
/** A session began: every pose anchor is pinned where it is. */
|
|
251
271
|
private _pinAll;
|
|
@@ -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;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { TouchStartEvent, TrackHandler } from "./touch";
|
|
2
|
+
/** One reading of a gesture, handed to `onStart` / `onMove` / `onEnd` (a fresh object each time). */
|
|
3
|
+
export interface Gesture {
|
|
4
|
+
/** The centroid of the live fingers, in the event's client coordinates (logical px). */
|
|
5
|
+
centerX: number;
|
|
6
|
+
centerY: number;
|
|
7
|
+
/** The centroid's move since the previous `onMove` (0 on start / end). */
|
|
8
|
+
deltaX: number;
|
|
9
|
+
deltaY: number;
|
|
10
|
+
/** Mean finger distance to the centroid over the previous one (1 with one finger, 1 on start / end). */
|
|
11
|
+
scale: number;
|
|
12
|
+
/** The mean change of each finger's angle about the centroid since the previous `onMove`, radians,
|
|
13
|
+
* positive = clockwise on screen (0 with one finger, 0 on start / end). */
|
|
14
|
+
rotation: number;
|
|
15
|
+
/** The number of live fingers (0 in `onEnd`: the last one lifted). */
|
|
16
|
+
pointers: number;
|
|
17
|
+
}
|
|
18
|
+
/** What `trackGesture` takes: the gesture's callbacks and the claim every finger's track makes. */
|
|
19
|
+
export interface GestureHandler {
|
|
20
|
+
/** Passed to every finger's `ev.track(...)` (default `true`: a scrollable above never takes the pan). */
|
|
21
|
+
claim?: TrackHandler["claim"];
|
|
22
|
+
/** The first finger landed. */
|
|
23
|
+
onStart?(g: Gesture): void;
|
|
24
|
+
/** Any finger moved: the change since the previous `onMove`. */
|
|
25
|
+
onMove?(g: Gesture): void;
|
|
26
|
+
/** The last finger lifted. */
|
|
27
|
+
onEnd?(g: Gesture): void;
|
|
28
|
+
/** The host cancelled one of the fingers: the whole gesture is over. */
|
|
29
|
+
onCancel?(): void;
|
|
30
|
+
}
|
|
31
|
+
/** Anything that hands out touchstarts: a UI element (`onTouchStart`) or a scene / node
|
|
32
|
+
* (`addEventListener("touchstart", …)`). */
|
|
33
|
+
export type GestureTarget = {
|
|
34
|
+
onTouchStart(callback: (ev: TouchStartEvent<any>) => void): unknown;
|
|
35
|
+
} | {
|
|
36
|
+
addEventListener(channel: "touchstart", callback: (ev: TouchStartEvent<any>) => void): void;
|
|
37
|
+
removeEventListener(channel: "touchstart", callback: (ev: TouchStartEvent<any>) => void): void;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Follow pan, pinch and twist on `target` as one gesture: every finger that lands on it is tracked,
|
|
41
|
+
* and `onMove` reports the change of the whole set since the previous move — the centroid's move,
|
|
42
|
+
* the spread's ratio, the twist's angle. One finger is a gesture too (a pan, scale 1, rotation 0).
|
|
43
|
+
*
|
|
44
|
+
* ```ts
|
|
45
|
+
* trackGesture(scene, {
|
|
46
|
+
* onMove(g) { zoom *= g.scale; angle += g.rotation; panX += g.deltaX; panY += g.deltaY },
|
|
47
|
+
* })
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export declare const trackGesture: (target: GestureTarget, handler: GestureHandler) => {
|
|
51
|
+
remove(): void;
|
|
52
|
+
};
|
|
@@ -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>;
|