lecodes-cli 0.18.2 → 0.19.2
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/index.js +1866 -301
- package/package.json +14 -12
- package/runtime/scene-harness.json +1 -1
- package/runtime/sdk-types.json +1 -1
- package/src/api.ts +302 -0
- package/src/browserAuth.ts +87 -0
- package/src/cmgenTool.ts +104 -0
- package/src/commands/app.ts +891 -0
- package/src/commands/appAndroid.ts +603 -0
- package/src/commands/appDesktop.ts +326 -0
- package/src/commands/appDesktopMac.ts +470 -0
- package/src/commands/appIcon.ts +187 -0
- package/src/commands/appShared.ts +448 -0
- package/src/commands/appTemplates.ts +883 -0
- package/src/commands/appTemplatesAndroid.ts +599 -0
- package/src/commands/appTemplatesGradlew.ts +9 -0
- package/src/commands/assets.ts +28 -0
- package/src/commands/clone.ts +60 -0
- package/src/commands/compile.ts +253 -0
- package/src/commands/create.ts +59 -0
- package/src/commands/design.ts +863 -0
- package/src/commands/designTemplates.ts +11 -0
- package/src/commands/desktop.ts +122 -0
- package/src/commands/dev.ts +214 -0
- package/src/commands/diff.ts +59 -0
- package/src/commands/init.ts +191 -0
- package/src/commands/install.ts +198 -0
- package/src/commands/lightmap.ts +290 -0
- package/src/commands/link.ts +147 -0
- package/src/commands/login.ts +24 -0
- package/src/commands/navmesh.ts +225 -0
- package/src/commands/pn.ts +305 -0
- package/src/commands/projectTemplates.ts +129 -0
- package/src/commands/pull.ts +109 -0
- package/src/commands/push.ts +173 -0
- package/src/commands/render.ts +414 -0
- package/src/commands/scene.ts +148 -0
- package/src/commands/shaders.ts +157 -0
- package/src/commands/status.ts +29 -0
- package/src/commands/test.ts +370 -0
- package/src/commands/thumbs.ts +178 -0
- package/src/commands/types.ts +87 -0
- package/src/commands/update.ts +190 -0
- package/src/compile/assetIcons.ts +214 -0
- package/src/compile/collect.ts +46 -0
- package/src/compile/collectLocal.ts +37 -0
- package/src/compile/designCompile.ts +104 -0
- package/src/compile/fonts.ts +156 -0
- package/src/compile/headlessBundle.ts +129 -0
- package/src/compile/nativeStack.ts +21 -0
- package/src/compile/projectCompile.ts +109 -0
- package/src/compile/sceneCompile.ts +113 -0
- package/src/compile/screenEntry.ts +127 -0
- package/src/compile/shaders.ts +245 -0
- package/src/config.ts +42 -0
- package/src/designMeta.ts +35 -0
- package/src/desktopRenderer.ts +532 -0
- package/src/desktopScript.ts +276 -0
- package/src/dev/clientTemplates.ts +160 -0
- package/src/dev/devServer.ts +292 -0
- package/src/dev/wsServer.ts +144 -0
- package/src/distRoot.ts +20 -0
- package/src/ignore.ts +163 -0
- package/src/index.ts +491 -0
- package/src/lecodes-3d-editor.d.ts +41 -0
- package/src/lecodes-assets.d.ts +7 -0
- package/src/lecodes-design.d.ts +191 -0
- package/src/lecodes-renderer.d.ts +131 -0
- package/src/localFiles.ts +144 -0
- package/src/manifest.ts +46 -0
- package/src/matcTool.ts +137 -0
- package/src/peers.ts +40 -0
- package/src/project.ts +61 -0
- package/src/projectEnv.ts +94 -0
- package/src/qrcode-terminal.d.ts +9 -0
- package/src/releases.ts +125 -0
- package/src/serverDiff.ts +78 -0
- package/src/textDiff.ts +103 -0
- package/src/types.ts +0 -0
- package/src/util.ts +146 -0
- package/runtime/sdk/animate/animate.ts +0 -238
- package/runtime/sdk/animate/bezier.ts +0 -138
- package/runtime/sdk/animate/easings.ts +0 -126
- package/runtime/sdk/canvas/Canvas.ts +0 -305
- package/runtime/sdk/core/Aspect.ts +0 -512
- package/runtime/sdk/core/InspectorUI.ts +0 -212
- package/runtime/sdk/core/color.ts +0 -66
- package/runtime/sdk/core/compWrite.ts +0 -42
- package/runtime/sdk/core/events.ts +0 -38
- package/runtime/sdk/core/fields.ts +0 -120
- package/runtime/sdk/core/registry.ts +0 -23
- package/runtime/sdk/core/signals.ts +0 -277
- package/runtime/sdk/core/time.ts +0 -81
- package/runtime/sdk/g2/Camera2D.ts +0 -40
- package/runtime/sdk/g2/CharacterController2D.ts +0 -276
- package/runtime/sdk/g2/Node2D.ts +0 -267
- package/runtime/sdk/g2/OneWay2D.ts +0 -66
- package/runtime/sdk/g2/Physics2D.ts +0 -346
- package/runtime/sdk/g2/Scene2D.ts +0 -209
- package/runtime/sdk/g2/Shape2D.ts +0 -259
- package/runtime/sdk/g2/Sprite.ts +0 -89
- package/runtime/sdk/g2/SpriteAnimation.ts +0 -171
- package/runtime/sdk/g2/SpriteSheet.ts +0 -166
- package/runtime/sdk/g2/Texture2D.ts +0 -47
- package/runtime/sdk/g2/Tilemap.ts +0 -41
- package/runtime/sdk/g2/Tileset.ts +0 -71
- package/runtime/sdk/g2/Trigger2D.ts +0 -77
- package/runtime/sdk/g2/autotile.ts +0 -433
- package/runtime/sdk/g2/cells.ts +0 -91
- package/runtime/sdk/g2/defineScene2d.ts +0 -381
- package/runtime/sdk/g2/groups2d.ts +0 -106
- package/runtime/sdk/g2/loop.ts +0 -50
- package/runtime/sdk/g2/scenarios2d.ts +0 -69
- package/runtime/sdk/g2/touch.ts +0 -83
- package/runtime/sdk/gl/Camera.ts +0 -160
- package/runtime/sdk/gl/CameraPlace.ts +0 -52
- package/runtime/sdk/gl/CharacterController.ts +0 -238
- package/runtime/sdk/gl/Gearbox.ts +0 -212
- package/runtime/sdk/gl/Geometry.ts +0 -279
- package/runtime/sdk/gl/IK.ts +0 -193
- package/runtime/sdk/gl/InstancedMesh.ts +0 -132
- package/runtime/sdk/gl/Light.ts +0 -99
- package/runtime/sdk/gl/Lightmap.ts +0 -179
- package/runtime/sdk/gl/Material.ts +0 -245
- package/runtime/sdk/gl/Mesh.ts +0 -83
- package/runtime/sdk/gl/Model.ts +0 -64
- package/runtime/sdk/gl/Node.ts +0 -350
- package/runtime/sdk/gl/Noise.ts +0 -30
- package/runtime/sdk/gl/Particles.ts +0 -676
- package/runtime/sdk/gl/Physics.ts +0 -222
- package/runtime/sdk/gl/Plane.ts +0 -53
- package/runtime/sdk/gl/Ray.ts +0 -16
- package/runtime/sdk/gl/Scene.ts +0 -479
- package/runtime/sdk/gl/Shape.ts +0 -377
- package/runtime/sdk/gl/Texture.ts +0 -46
- package/runtime/sdk/gl/Trigger.ts +0 -45
- package/runtime/sdk/gl/Vehicle.ts +0 -473
- package/runtime/sdk/gl/Wheel.ts +0 -240
- package/runtime/sdk/gl/animation/AnimationClip.ts +0 -204
- package/runtime/sdk/gl/animation/Animator.ts +0 -87
- package/runtime/sdk/gl/animation/Layer.ts +0 -29
- package/runtime/sdk/gl/animation/Loop.ts +0 -25
- package/runtime/sdk/gl/animation/Playback.ts +0 -43
- package/runtime/sdk/gl/animation/core.ts +0 -294
- package/runtime/sdk/gl/controls.ts +0 -95
- package/runtime/sdk/gl/physicsEvents.ts +0 -20
- package/runtime/sdk/gl/scenarios.ts +0 -291
- package/runtime/sdk/gl/state.ts +0 -6
- package/runtime/sdk/gl/touch.ts +0 -68
- package/runtime/sdk/inject.ts +0 -186
- package/runtime/sdk/math/Mathf.ts +0 -118
- package/runtime/sdk/math/mat4.ts +0 -278
- package/runtime/sdk/math/quat.ts +0 -232
- package/runtime/sdk/math/vec.ts +0 -255
- package/runtime/sdk/plugins/camera.ts +0 -81
- package/runtime/sdk/plugins/geolocation.ts +0 -123
- package/runtime/sdk/plugins/oauth.ts +0 -61
- package/runtime/sdk/plugins/permission.ts +0 -7
- package/runtime/sdk/plugins/push.ts +0 -132
- package/runtime/sdk/plugins/qr.ts +0 -73
- package/runtime/sdk/plugins/service.ts +0 -47
- package/runtime/sdk/runtime/app.ts +0 -101
- package/runtime/sdk/runtime/appEvents.ts +0 -54
- package/runtime/sdk/runtime/channel.ts +0 -61
- package/runtime/sdk/runtime/clipboard.ts +0 -20
- package/runtime/sdk/runtime/datetime.ts +0 -329
- package/runtime/sdk/runtime/device.ts +0 -293
- package/runtime/sdk/runtime/fetch.ts +0 -77
- package/runtime/sdk/runtime/files.ts +0 -23
- package/runtime/sdk/runtime/input.ts +0 -175
- package/runtime/sdk/runtime/media.ts +0 -111
- package/runtime/sdk/runtime/misc.ts +0 -16
- package/runtime/sdk/runtime/net.ts +0 -36
- package/runtime/sdk/runtime/rpc.ts +0 -218
- package/runtime/sdk/runtime/service.ts +0 -83
- package/runtime/sdk/runtime/share.ts +0 -9
- package/runtime/sdk/runtime/storage.ts +0 -13
- package/runtime/sdk/runtime/touch.ts +0 -76
- package/runtime/sdk/scene/defineScene.ts +0 -1227
- package/runtime/sdk/scene/editorPlugins.ts +0 -92
- package/runtime/sdk/scene/gizmos.ts +0 -148
- package/runtime/sdk/scene/grammar.ts +0 -120
- package/runtime/sdk/scene/material.ts +0 -188
- package/runtime/sdk/server/auth/appConfig.ts +0 -12
- package/runtime/sdk/server/auth/global.ts +0 -80
- package/runtime/sdk/server/auth/host.ts +0 -318
- package/runtime/sdk/server/auth/models.ts +0 -83
- package/runtime/sdk/server/auth/types.ts +0 -50
- package/runtime/sdk/server/channel.ts +0 -56
- package/runtime/sdk/server/context.ts +0 -36
- package/runtime/sdk/server/db/defineDb.ts +0 -237
- package/runtime/sdk/server/db/fields.ts +0 -132
- package/runtime/sdk/server/db/httpTransport.ts +0 -93
- package/runtime/sdk/server/db/index.ts +0 -7
- package/runtime/sdk/server/db/marci/query.ts +0 -412
- package/runtime/sdk/server/db/types.ts +0 -202
- package/runtime/sdk/server/errors.ts +0 -12
- package/runtime/sdk/server/host.ts +0 -74
- package/runtime/sdk/server/inject.ts +0 -13
- package/runtime/sdk/server/runtime.ts +0 -133
- package/runtime/sdk/server/validate.ts +0 -87
- package/runtime/sdk/ui/NativeView.ts +0 -142
- package/runtime/sdk/ui/UI.ts +0 -39
- package/runtime/sdk/ui/UIBottomSheet.ts +0 -139
- package/runtime/sdk/ui/UIButton.ts +0 -101
- package/runtime/sdk/ui/UIContainer.ts +0 -60
- package/runtime/sdk/ui/UIImage.ts +0 -83
- package/runtime/sdk/ui/UIInput.ts +0 -185
- package/runtime/sdk/ui/UIModal.ts +0 -139
- package/runtime/sdk/ui/UINode.ts +0 -826
- package/runtime/sdk/ui/UIPager.ts +0 -362
- package/runtime/sdk/ui/UIPopover.ts +0 -100
- package/runtime/sdk/ui/UIScreen.ts +0 -123
- package/runtime/sdk/ui/UIScrollable.ts +0 -87
- package/runtime/sdk/ui/UISpacer.ts +0 -14
- package/runtime/sdk/ui/UITabs.ts +0 -236
- package/runtime/sdk/ui/UIText.ts +0 -51
- package/runtime/sdk/ui/UIVideo.ts +0 -88
- package/runtime/sdk/ui/UIVirtualizedList.ts +0 -241
- package/runtime/sdk/ui/UIWidget.ts +0 -127
- package/runtime/sdk/ui/fonts.ts +0 -13
- package/runtime/sdk/ui/presentable.ts +0 -117
- package/runtime/sdk/ui/router.ts +0 -132
- package/runtime/sdk/ui/theme.ts +0 -84
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
// HTTP via the host. `fetch` is async (host streams the body), `fetchLocal` reads a preloaded asset
|
|
2
|
-
// synchronously. Ported from worker/src/fetch.ts over the same _creatorUtils bridge.
|
|
3
|
-
|
|
4
|
-
export type FetchOptions = {
|
|
5
|
-
body?: any
|
|
6
|
-
method?: string
|
|
7
|
-
headers?: Record<string, string>
|
|
8
|
-
useOnce?: boolean
|
|
9
|
-
onProgress?: (progress: { loaded: number, total?: number }) => void
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface FetchResponse {
|
|
13
|
-
readonly status: number
|
|
14
|
-
json<T = any>(): T
|
|
15
|
-
text(): string
|
|
16
|
-
dispose(): void
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
class FetchResponseImpl implements FetchResponse {
|
|
20
|
-
readonly status: number
|
|
21
|
-
private readonly _id: number
|
|
22
|
-
constructor(id: number, status: number) {
|
|
23
|
-
this._id = id
|
|
24
|
-
this.status = status
|
|
25
|
-
}
|
|
26
|
-
json<T = any>(): T {
|
|
27
|
-
return _creatorUtils.fetchToJson(this._id)
|
|
28
|
-
}
|
|
29
|
-
text(): string {
|
|
30
|
-
return _creatorUtils.fetchToText(this._id)
|
|
31
|
-
}
|
|
32
|
-
dispose(): void {
|
|
33
|
-
_creatorUtils.disposeFetch(this._id)
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export const fetch = (url: string, options: FetchOptions = {}): Promise<FetchResponse> =>
|
|
38
|
-
new Promise<FetchResponse>((resolve, reject) => {
|
|
39
|
-
_creatorUtils.fetch(url, options, (systemId, statusCode) => {
|
|
40
|
-
resolve(new FetchResponseImpl(systemId, statusCode))
|
|
41
|
-
}, reject)
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
export const fetchLocal = (path: string): FetchResponse =>
|
|
45
|
-
new FetchResponseImpl(_creatorUtils.fetchLocal(path), 200)
|
|
46
|
-
|
|
47
|
-
/** A handle to a picked / fetched file (see openFilePicker). */
|
|
48
|
-
export class File {
|
|
49
|
-
/** @internal host buffer handle. `_`-prefixed because hosts read it off the raw object —
|
|
50
|
-
* same key `FetchResponse` carries, so both are accepted anywhere a buffer is expected
|
|
51
|
-
* (FormData entries, `UIImage` src, `Texture.load`, `share`). */
|
|
52
|
-
readonly _id: number
|
|
53
|
-
readonly name: string
|
|
54
|
-
readonly size: number
|
|
55
|
-
constructor(systemId: number, name: string, size: number) {
|
|
56
|
-
this._id = systemId
|
|
57
|
-
this.name = name
|
|
58
|
-
this.size = size
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
type FormDataValue = string | number | boolean | File | FetchResponse
|
|
63
|
-
|
|
64
|
-
export class FormData {
|
|
65
|
-
// Host-facing: hosts walk `_entries` and treat any object value carrying `_id` as binary
|
|
66
|
-
// (the buffer id), everything else as a text field. See viewer-lite utils/fetch.ts,
|
|
67
|
-
// creator-pkg creatorUtilsHandler.cpp, iOS Fetch.swift buildMultipartBody.
|
|
68
|
-
private _entries: [string, FormDataValue, filename?: string][] = []
|
|
69
|
-
|
|
70
|
-
append(name: string, value: FormDataValue, filename?: string): void {
|
|
71
|
-
this._entries.push([ name, value, filename ?? (typeof value === "object" ? (value as any).name : undefined) ])
|
|
72
|
-
}
|
|
73
|
-
delete(name: string): void {
|
|
74
|
-
const index = this._entries.findIndex((i) => i[0] === name)
|
|
75
|
-
if (index >= 0) this._entries.splice(index, 1)
|
|
76
|
-
}
|
|
77
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
// Native file picker. Ported from worker/src/utils/openFilePicker.ts; resolves to File handle(s)
|
|
2
|
-
// (see runtime/fetch.ts) that can be appended to a FormData and uploaded.
|
|
3
|
-
|
|
4
|
-
import { File } from "./fetch"
|
|
5
|
-
|
|
6
|
-
type OpenFilePickerOptions = { accept?: string }
|
|
7
|
-
|
|
8
|
-
export function openFilePicker(options?: OpenFilePickerOptions & { multiple?: false }): Promise<File | null>
|
|
9
|
-
export function openFilePicker(options: OpenFilePickerOptions & { multiple: true }): Promise<File[]>
|
|
10
|
-
export function openFilePicker(
|
|
11
|
-
options: OpenFilePickerOptions & { multiple?: boolean } = {},
|
|
12
|
-
): Promise<File | File[] | null> {
|
|
13
|
-
return new Promise((resolve, reject) => {
|
|
14
|
-
_creatorUtils.openFilePicker((resp) => {
|
|
15
|
-
if (!resp || resp.length === 0) {
|
|
16
|
-
resolve(options.multiple ? [] : null)
|
|
17
|
-
return
|
|
18
|
-
}
|
|
19
|
-
if (options.multiple) resolve(resp.map((i: any) => new File(i[0], i[1], i[2])))
|
|
20
|
-
else resolve(new File(resp[0][0], resp[0][1], resp[0][2]))
|
|
21
|
-
}, reject, !!options.multiple, options.accept)
|
|
22
|
-
})
|
|
23
|
-
}
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
// Keyboard / mouse / gamepad input behind one `Input` global (docs/input-plan.md).
|
|
2
|
-
//
|
|
3
|
-
// Two kinds of signal, two shapes of API:
|
|
4
|
-
// - BUTTONS — anything that goes down and up: keyboard keys, mouse buttons, gamepad buttons. One
|
|
5
|
-
// code vocabulary ('KeyW', 'MouseLeft', 'GamepadSouth'), one held-poll (`Input.key`) and one
|
|
6
|
-
// event pair (`Input.on('keydown' | 'keyup')`). A mouse button IS a key.
|
|
7
|
-
// - CONTINUOUS — mouse motion/wheel, sticks, triggers: polled every frame (`Input.mouse.delta`,
|
|
8
|
-
// `Input.gamepad(0).axis('leftX')`), never events.
|
|
9
|
-
// Bindings ("jump = Space | GamepadSouth") are app code: compare `e.code`, OR a few `Input.key`s.
|
|
10
|
-
// Hosts report physical state only (`_creatorUtils.inputKey/inputRead/registerInputEvent`).
|
|
11
|
-
|
|
12
|
-
/** Continuous channel ids — the `_creatorUtils.inputRead` contract (bridges.d.ts). */
|
|
13
|
-
export const InputChannel = {
|
|
14
|
-
MouseX: 0, MouseY: 1, MouseDX: 2, MouseDY: 3, WheelX: 4, WheelY: 5, PointerLocked: 6,
|
|
15
|
-
GamepadCount: 7,
|
|
16
|
-
GamepadBase: 16, GamepadStride: 16,
|
|
17
|
-
PadConnected: 0, PadLeftX: 1, PadLeftY: 2, PadRightX: 3, PadRightY: 4, PadLeftTrigger: 5, PadRightTrigger: 6,
|
|
18
|
-
} as const
|
|
19
|
-
|
|
20
|
-
export type InputEventName = "keydown" | "keyup" | "gamepadconnected" | "gamepaddisconnected"
|
|
21
|
-
|
|
22
|
-
/** `keydown` / `keyup` payload. `code` is the same string `Input.key()` takes. */
|
|
23
|
-
export interface InputKeyEvent {
|
|
24
|
-
readonly type: "keydown" | "keyup"
|
|
25
|
-
/** Button code: KeyboardEvent.code ('KeyW', 'Space'), 'MouseLeft|Right|Middle|Back|Forward', or
|
|
26
|
-
* 'GamepadSouth|East|West|North|L1|R1|L2|R2|Select|Start|L3|R3|Up|Down|Left|Right'. */
|
|
27
|
-
readonly code: string
|
|
28
|
-
/** Which device: 'keyboard' | 'mouse' | 'gamepad'. */
|
|
29
|
-
readonly source: "keyboard" | "mouse" | "gamepad"
|
|
30
|
-
/** Pad index (0..3) for gamepad buttons, undefined otherwise. */
|
|
31
|
-
readonly gamepad?: number
|
|
32
|
-
/** OS auto-repeat keydown (key still held) — ignore for one-shot actions. */
|
|
33
|
-
readonly repeat: boolean
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface InputGamepadEvent {
|
|
37
|
-
readonly type: "gamepadconnected" | "gamepaddisconnected"
|
|
38
|
-
readonly gamepad: number
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
type Listener<E> = (e: E) => void
|
|
42
|
-
type InputEventMap = {
|
|
43
|
-
keydown: InputKeyEvent
|
|
44
|
-
keyup: InputKeyEvent
|
|
45
|
-
gamepadconnected: InputGamepadEvent
|
|
46
|
-
gamepaddisconnected: InputGamepadEvent
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const listeners: { [K in InputEventName]?: Listener<InputEventMap[K]>[] } = {}
|
|
50
|
-
let hostListenerInstalled = false
|
|
51
|
-
|
|
52
|
-
const sourceOf = (code: string): InputKeyEvent["source"] =>
|
|
53
|
-
code.startsWith("Mouse") ? "mouse" : code.startsWith("Gamepad") ? "gamepad" : "keyboard"
|
|
54
|
-
|
|
55
|
-
const emit = <K extends InputEventName>(name: K, e: InputEventMap[K]) => {
|
|
56
|
-
const list = listeners[name]
|
|
57
|
-
if (!list || list.length === 0) return
|
|
58
|
-
for (const fn of [...list]) fn(e as any)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// One fan-out for the host channel and the test/host `_emit` seam.
|
|
62
|
-
const deliver = (kind: number, code: string, gamepad: number, repeat: number) => {
|
|
63
|
-
if (kind === 0 || kind === 1) {
|
|
64
|
-
const src = sourceOf(code)
|
|
65
|
-
emit(kind === 0 ? "keydown" : "keyup", {
|
|
66
|
-
type: kind === 0 ? "keydown" : "keyup", code, source: src,
|
|
67
|
-
gamepad: src === "gamepad" && gamepad >= 0 ? gamepad : undefined, repeat: repeat !== 0,
|
|
68
|
-
})
|
|
69
|
-
} else if (kind === 2 || kind === 3) {
|
|
70
|
-
emit(kind === 2 ? "gamepadconnected" : "gamepaddisconnected",
|
|
71
|
-
{ type: kind === 2 ? "gamepadconnected" : "gamepaddisconnected", gamepad })
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Lazily claim the host's single listener slot (never at module scope — chisel drops top-level
|
|
76
|
-
// side effects, and a host without the channel simply never fires).
|
|
77
|
-
const ensureHostListener = () => {
|
|
78
|
-
if (hostListenerInstalled) return
|
|
79
|
-
hostListenerInstalled = true
|
|
80
|
-
_creatorUtils.registerInputEvent?.(deliver)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const read = (channel: number): number => _creatorUtils.inputRead?.(channel) ?? 0
|
|
84
|
-
|
|
85
|
-
/** Mouse state — polled. Position is the cursor inside the viewport in logical px; `delta` is the
|
|
86
|
-
* motion during the previous frame (raw where the OS offers it) and keeps counting while locked —
|
|
87
|
-
* that's FPS look. Hosts without a mouse report zeros. */
|
|
88
|
-
export const mouse = {
|
|
89
|
-
get position(): { x: number, y: number } { return { x: read(InputChannel.MouseX), y: read(InputChannel.MouseY) } },
|
|
90
|
-
get delta(): { x: number, y: number } { return { x: read(InputChannel.MouseDX), y: read(InputChannel.MouseDY) } },
|
|
91
|
-
/** Wheel notches during the previous frame (+y = wheel toward you / scroll down). */
|
|
92
|
-
get wheel(): { x: number, y: number } { return { x: read(InputChannel.WheelX), y: read(InputChannel.WheelY) } },
|
|
93
|
-
/** Pointer lock: cursor hidden + confined, `delta` keeps flowing. Web grants it only from a user
|
|
94
|
-
* gesture (call it in a 'keydown' handler for MouseLeft); hosts drop it on focus loss and
|
|
95
|
-
* re-acquire on focus. Escape does NOT unlock by itself on native hosts — call `unlock()`. */
|
|
96
|
-
get locked(): boolean { return read(InputChannel.PointerLocked) !== 0 },
|
|
97
|
-
lock(): boolean { return _creatorUtils.inputSetPointerLock?.(true) ?? false },
|
|
98
|
-
unlock(): void { _creatorUtils.inputSetPointerLock?.(false) },
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export type GamepadAxisName = "leftX" | "leftY" | "rightX" | "rightY" | "leftTrigger" | "rightTrigger"
|
|
102
|
-
const PAD_AXIS: Record<GamepadAxisName, number> = {
|
|
103
|
-
leftX: InputChannel.PadLeftX, leftY: InputChannel.PadLeftY, rightX: InputChannel.PadRightX,
|
|
104
|
-
rightY: InputChannel.PadRightY, leftTrigger: InputChannel.PadLeftTrigger, rightTrigger: InputChannel.PadRightTrigger,
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export interface GamepadState {
|
|
108
|
-
readonly index: number
|
|
109
|
-
readonly connected: boolean
|
|
110
|
-
/** Stick axes −1..1 (+Y = down, like the web Gamepad API), triggers 0..1. `deadzone` (default
|
|
111
|
-
* 0.15) zeroes small stick drift — pass 0 for the raw value. */
|
|
112
|
-
axis(name: GamepadAxisName, deadzone?: number): number
|
|
113
|
-
/** Held? Same as `Input.key(code, index)`. */
|
|
114
|
-
button(code: string): boolean
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const padCache: GamepadState[] = []
|
|
118
|
-
const gamepadAt = (index: number): GamepadState => {
|
|
119
|
-
let g = padCache[index]
|
|
120
|
-
if (!g) {
|
|
121
|
-
const base = InputChannel.GamepadBase + InputChannel.GamepadStride * index
|
|
122
|
-
g = padCache[index] = {
|
|
123
|
-
index,
|
|
124
|
-
get connected() { return read(base + InputChannel.PadConnected) !== 0 },
|
|
125
|
-
axis(name, deadzone = 0.15) {
|
|
126
|
-
const v = read(base + PAD_AXIS[name])
|
|
127
|
-
return deadzone > 0 && Math.abs(v) < deadzone ? 0 : v
|
|
128
|
-
},
|
|
129
|
-
button(code) { return _creatorUtils.inputKey(code, index) },
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
return g
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export const Input = {
|
|
136
|
-
/**
|
|
137
|
-
* Is a button held? `code` is the physical key (`KeyboardEvent.code`: 'ArrowRight', 'KeyW',
|
|
138
|
-
* 'Space'), a mouse button ('MouseLeft', 'MouseRight', 'MouseMiddle') or a gamepad button
|
|
139
|
-
* ('GamepadSouth' = A/Cross, 'GamepadR2' = right trigger as a button …). Poll inside setLoop for
|
|
140
|
-
* frame-independent movement. `gamepad` scopes a Gamepad* code to one pad; omitted = any pad.
|
|
141
|
-
*/
|
|
142
|
-
key(code: string, gamepad?: number): boolean {
|
|
143
|
-
return _creatorUtils.inputKey(code, gamepad)
|
|
144
|
-
},
|
|
145
|
-
|
|
146
|
-
/** Listen for a discrete input moment. `keydown`/`keyup` cover EVERY button — keyboard, mouse,
|
|
147
|
-
* gamepad — filter on `e.code`. Use these for one-shot actions (jump, shoot, charge-release);
|
|
148
|
-
* `Input.key` for continuous ones (walk). */
|
|
149
|
-
on<K extends InputEventName>(name: K, listener: Listener<InputEventMap[K]>): void {
|
|
150
|
-
ensureHostListener()
|
|
151
|
-
const list = (listeners[name] ??= []) as Listener<InputEventMap[K]>[]
|
|
152
|
-
if (!list.includes(listener)) list.push(listener)
|
|
153
|
-
},
|
|
154
|
-
off<K extends InputEventName>(name: K, listener: Listener<InputEventMap[K]>): void {
|
|
155
|
-
const list = listeners[name] as Listener<InputEventMap[K]>[] | undefined
|
|
156
|
-
if (!list) return
|
|
157
|
-
const i = list.indexOf(listener)
|
|
158
|
-
if (i >= 0) list.splice(i, 1)
|
|
159
|
-
},
|
|
160
|
-
|
|
161
|
-
mouse,
|
|
162
|
-
|
|
163
|
-
/** Gamepad `index` (0..3). Always returns an object — check `.connected`. */
|
|
164
|
-
gamepad(index = 0): GamepadState { return gamepadAt(index) },
|
|
165
|
-
/** Indices of the currently connected gamepads. */
|
|
166
|
-
gamepads(): number[] {
|
|
167
|
-
const out: number[] = []
|
|
168
|
-
const n = read(InputChannel.GamepadCount)
|
|
169
|
-
for (let i = 0; i < 4 && out.length < n; i++) if (gamepadAt(i).connected) out.push(i)
|
|
170
|
-
return out
|
|
171
|
-
},
|
|
172
|
-
|
|
173
|
-
/** @internal — hosts/tests: deliver an event exactly as the host listener would. */
|
|
174
|
-
_emit(kind: number, code: string, gamepad: number, repeat: number): void { deliver(kind, code, gamepad, repeat) },
|
|
175
|
-
}
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
// Audio / video playback over the host. Ported from worker/src/MediaPlayer.ts, with the listener
|
|
2
|
-
// boilerplate folded into the shared Emitter. VideoPlayer.texture (a 3D Texture) is wired in Phase 4
|
|
3
|
-
// when the Texture type exists — see gl/.
|
|
4
|
-
//
|
|
5
|
-
// The host no longer routes end-of-playback events back by id through a Registry: each player hands
|
|
6
|
-
// the host a `_finished` callback at creation time. The host holds the JS closure directly — valid
|
|
7
|
-
// on every backend (web JS, Android quickjs, iOS JavaScriptCore) — and invokes it when a play-through
|
|
8
|
-
// completes, so there is no id table to keep in sync.
|
|
9
|
-
|
|
10
|
-
import { Emitter } from "../core/events"
|
|
11
|
-
import { Texture } from "../gl/Texture"
|
|
12
|
-
|
|
13
|
-
type MediaEvents = {
|
|
14
|
-
/** Fired at each loop boundary while `loop` is true. */
|
|
15
|
-
loopReached: () => void
|
|
16
|
-
/** Fired once when playback reaches the end while `loop` is false. */
|
|
17
|
-
completed: () => void
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
abstract class MediaPlayer extends Emitter<MediaEvents> {
|
|
21
|
-
protected _id = -1
|
|
22
|
-
protected _src = ""
|
|
23
|
-
private _playing = false
|
|
24
|
-
private _loop = false
|
|
25
|
-
private _volume = 1
|
|
26
|
-
private _disposed = false
|
|
27
|
-
|
|
28
|
-
// Every host-touching operation runs through this so a disposed player can never address a freed
|
|
29
|
-
// (and possibly reused) host slot.
|
|
30
|
-
private _assertAlive(): void {
|
|
31
|
-
if (this._disposed) throw new Error("MediaPlayer has been disposed")
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
play(): void { this.playing = true }
|
|
35
|
-
pause(): void { this.playing = false }
|
|
36
|
-
|
|
37
|
-
get playing(): boolean { return this._playing }
|
|
38
|
-
set playing(value: boolean) {
|
|
39
|
-
this._assertAlive()
|
|
40
|
-
this._playing = value
|
|
41
|
-
_creatorUtils.updateMediaPlayerPlaying(this._id, value)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
get volume(): number { return this._volume }
|
|
45
|
-
set volume(value: number) {
|
|
46
|
-
this._assertAlive()
|
|
47
|
-
this._volume = value
|
|
48
|
-
_creatorUtils.updateMediaPlayerVolume(this._id, value)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
get loop(): boolean { return this._loop }
|
|
52
|
-
set loop(value: boolean) {
|
|
53
|
-
this._assertAlive()
|
|
54
|
-
this._loop = value
|
|
55
|
-
_creatorUtils.updateMediaPlayerLoop(this._id, value)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Current playback position, in seconds. Settable to seek. */
|
|
59
|
-
get time(): number { this._assertAlive(); return _creatorUtils.getMediaPlayerTime(this._id) }
|
|
60
|
-
set time(value: number) { this._assertAlive(); _creatorUtils.setMediaPlayerTime(this._id, value) }
|
|
61
|
-
|
|
62
|
-
/** Total media length in seconds (0 until metadata has loaded). */
|
|
63
|
-
get duration(): number { this._assertAlive(); return _creatorUtils.getMediaPlayerDuration(this._id) }
|
|
64
|
-
|
|
65
|
-
/** True once {@link dispose} has been called; the player is unusable afterwards. */
|
|
66
|
-
get disposed(): boolean { return this._disposed }
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Release the underlying host media resource (decoder, network, buffers) immediately and detach
|
|
70
|
-
* all listeners. Idempotent. Any further use of the player throws.
|
|
71
|
-
*/
|
|
72
|
-
dispose(): void {
|
|
73
|
-
if (this._disposed) return
|
|
74
|
-
this._disposed = true
|
|
75
|
-
this._playing = false
|
|
76
|
-
this.clearListeners()
|
|
77
|
-
_creatorUtils.removeMediaPlayer(this._id)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** @internal — the host calls this when a play-through finishes. */
|
|
81
|
-
protected _finished(): void {
|
|
82
|
-
if (this._disposed) return
|
|
83
|
-
if (this._loop) {
|
|
84
|
-
this.dispatch("loopReached")
|
|
85
|
-
} else {
|
|
86
|
-
this._playing = false
|
|
87
|
-
this.dispatch("completed")
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export class AudioPlayer extends MediaPlayer {
|
|
93
|
-
constructor(src?: string) {
|
|
94
|
-
super()
|
|
95
|
-
this._src = src ?? ""
|
|
96
|
-
this._id = _creatorUtils.createAudioPlayer(this._src, () => this._finished())
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export class VideoPlayer extends MediaPlayer {
|
|
101
|
-
constructor(src?: string) {
|
|
102
|
-
super()
|
|
103
|
-
this._src = src ?? ""
|
|
104
|
-
this._id = _creatorUtils.createVideoPlayer(this._src, () => this._finished())
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** A 3D Texture that samples this video (e.g. for a Material.video()). */
|
|
108
|
-
get texture(): Texture {
|
|
109
|
-
return new Texture(0, 0, _creator.createMediaPlayerTexture(this._id))
|
|
110
|
-
}
|
|
111
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
// Small platform globals that are real (not engine-specific). Grown in Phase 1.
|
|
2
|
-
|
|
3
|
-
/** Show a transient toast notification. */
|
|
4
|
-
export const toast = (msg: string): void => _creatorUI.toast(msg)
|
|
5
|
-
|
|
6
|
-
/** Open `url` in the system browser / external handler (fire-and-forget, like `share`). Silent
|
|
7
|
-
* no-op on hosts without one (headless). */
|
|
8
|
-
export const openURL = (url: string): void => _creatorUtils.openUrl?.(url)
|
|
9
|
-
|
|
10
|
-
export type SvgSourceValue = {
|
|
11
|
-
readonly svg: string
|
|
12
|
-
tintColor: string | null
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** Wrap raw SVG XML so it can be used as an image source. */
|
|
16
|
-
export const SvgSource = (svg: string): SvgSourceValue => ({ svg, tintColor: null })
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
// WebSocket over the host bridge. Same event surface as the browser's (open/message/close/error)
|
|
2
|
-
// via the shared Emitter. Ported from worker/src/WebSocket.ts.
|
|
3
|
-
|
|
4
|
-
import { Emitter } from "../core/events"
|
|
5
|
-
|
|
6
|
-
type SocketEvents = {
|
|
7
|
-
open: () => void
|
|
8
|
-
message: (data: any) => void
|
|
9
|
-
close: (code?: number) => void
|
|
10
|
-
error: () => void
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export class WebSocket extends Emitter<SocketEvents> {
|
|
14
|
-
private _id: number | null = null
|
|
15
|
-
|
|
16
|
-
constructor(url: string, headers?: Record<string, string>) {
|
|
17
|
-
super()
|
|
18
|
-
this._id = _creatorUtils.websocketOpen(url, (channel, data) => this._onMessage(channel, data), headers)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
send(message: string | ArrayBuffer): void {
|
|
22
|
-
if (this._id !== null) _creatorUtils.websocketSend(this._id, message)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
close(): void {
|
|
26
|
-
if (this._id !== null) {
|
|
27
|
-
_creatorUtils.websocketClose(this._id)
|
|
28
|
-
this._id = null
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
private _onMessage(channel: string, data: any): void {
|
|
33
|
-
if (data !== undefined && data !== null) this.dispatch(channel as keyof SocketEvents, data)
|
|
34
|
-
else this.dispatch(channel as keyof SocketEvents)
|
|
35
|
-
}
|
|
36
|
-
}
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Client transport for the app backend (docs/backend-plan.md §3.2 / §3.3). The compiler replaces
|
|
3
|
-
* every `*.server.ts` module in the client bundle with a stub whose exports are built by the three
|
|
4
|
-
* globals below — user code never calls them; it imports the server function and calls it.
|
|
5
|
-
*
|
|
6
|
-
* export const listNotes = __rpc("<serverUrl>", "notes.server.ts#listNotes")
|
|
7
|
-
* export const chat = __channel("<serverUrl>", "chat.server.ts#chat")
|
|
8
|
-
* export const db = __serverOnly("schema.server.ts#db")
|
|
9
|
-
*
|
|
10
|
-
* Wire (implemented by the runner):
|
|
11
|
-
* POST <serverUrl>/api/<id> body {"args":[…]}, `authorization: Bearer <session>` when known
|
|
12
|
-
* → 200 {"ok":true,"result":…,"session"?:"…"} | {"ok":false,"status":n,"message":"…","session"?:"…"}
|
|
13
|
-
* WS <serverUrl>/ws frames: → {t:"hello",session} {t:"sub",id,ch,topic} {t:"unsub",id}
|
|
14
|
-
* ← {t:"session",session} {t:"ok",id} {t:"err",id,status,message} {t:"ev",ch,topic,event,data}
|
|
15
|
-
* (`ev` is addressed by channel + topic, not by subscription id, so the server can fan one publish out
|
|
16
|
-
* to every socket of a topic natively; the client delivers it to each matching subscription.)
|
|
17
|
-
*
|
|
18
|
-
* The session token is transport-owned (cookie-like): stored under `lecodes.session:<serverUrl>` in
|
|
19
|
-
* localStorage, sent on every call and on the socket, replaced whenever a response carries `session`.
|
|
20
|
-
* User code never sees it — "am I logged in" is an endpoint (`me()`), see the plan.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import { fetch } from "./fetch"
|
|
24
|
-
import { localStorage } from "./storage"
|
|
25
|
-
import { WebSocket } from "./net"
|
|
26
|
-
|
|
27
|
-
/** Rejection value of a failed endpoint call: `status` mirrors the server's `ApiError`
|
|
28
|
-
* (0 = the request itself failed: no network / no server). */
|
|
29
|
-
export class RpcError extends Error {
|
|
30
|
-
readonly status: number
|
|
31
|
-
constructor(status: number, message: string) {
|
|
32
|
-
super(message)
|
|
33
|
-
this.name = "RpcError"
|
|
34
|
-
this.status = status
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// ───────────────────────────── session token ─────────────────────────────
|
|
39
|
-
|
|
40
|
-
const sessionKey = (serverUrl: string) => `lecodes.session:${serverUrl}`
|
|
41
|
-
const getSession = (serverUrl: string): string | null => {
|
|
42
|
-
try { return localStorage.getItem(sessionKey(serverUrl)) } catch { return null }
|
|
43
|
-
}
|
|
44
|
-
const setSession = (serverUrl: string, token: string | null) => {
|
|
45
|
-
try {
|
|
46
|
-
if (token) localStorage.setItem(sessionKey(serverUrl), token)
|
|
47
|
-
else localStorage.removeItem(sessionKey(serverUrl))
|
|
48
|
-
} catch { /* storage-less host: the session lives for this run only */ }
|
|
49
|
-
sockets.get(serverUrl)?.onSessionChanged(token)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// ───────────────────────────── endpoints ─────────────────────────────
|
|
53
|
-
|
|
54
|
-
const noServer = (id: string) => new RpcError(0, `Endpoint ${id}: the project has no deployed server (compile with serverUrl)`)
|
|
55
|
-
|
|
56
|
-
/** Build the client proxy of endpoint `id`. Positional args travel as a JSON array. */
|
|
57
|
-
export const __rpc = (serverUrl: string, id: string) => {
|
|
58
|
-
const call = async (...args: unknown[]): Promise<any> => {
|
|
59
|
-
if (!serverUrl) throw noServer(id)
|
|
60
|
-
const headers: Record<string, string> = { "content-type": "application/json" }
|
|
61
|
-
const session = getSession(serverUrl)
|
|
62
|
-
if (session) headers.authorization = `Bearer ${session}`
|
|
63
|
-
let res
|
|
64
|
-
try {
|
|
65
|
-
res = await fetch(`${serverUrl}/api/${encodeURIComponent(id)}`, { method: "POST", headers, body: JSON.stringify({ args }) })
|
|
66
|
-
} catch (e) {
|
|
67
|
-
throw new RpcError(0, `Endpoint ${id}: request failed (${(e as Error)?.message ?? e})`)
|
|
68
|
-
}
|
|
69
|
-
let body: any
|
|
70
|
-
try { body = res.json() } catch { body = null }
|
|
71
|
-
res.dispose()
|
|
72
|
-
if (body && typeof body === "object" && "session" in body) setSession(serverUrl, body.session ?? null)
|
|
73
|
-
if (body && body.ok === true) return body.result
|
|
74
|
-
if (body && body.ok === false) throw new RpcError(body.status ?? res.status, body.message ?? "")
|
|
75
|
-
throw new RpcError(res.status, `Endpoint ${id}: unexpected response (HTTP ${res.status})`)
|
|
76
|
-
}
|
|
77
|
-
return call
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** A server-only export (db, models, helpers) referenced from client code: any use throws. */
|
|
81
|
-
export const __serverOnly = (id: string): any =>
|
|
82
|
-
new Proxy(function () {} as any, {
|
|
83
|
-
get: (_t, prop) => { if (prop === "then") return undefined; throw new RpcError(0, `${id} is server-only — it can't be used in client code`) },
|
|
84
|
-
apply: () => { throw new RpcError(0, `${id} is server-only — it can't be used in client code`) },
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
// ───────────────────────────── channels ─────────────────────────────
|
|
88
|
-
|
|
89
|
-
type Handlers = Record<string, ((payload: any) => void) | undefined> & {
|
|
90
|
-
/** The socket dropped and came back: subscriptions were re-sent, but events in between are lost — refetch. */
|
|
91
|
-
reconnect?: () => void
|
|
92
|
-
/** The subscription was refused (`onJoin` threw) or the socket can't connect. */
|
|
93
|
-
error?: (e: RpcError) => void
|
|
94
|
-
}
|
|
95
|
-
type Sub = { id: number, ch: string, topic: string, handlers: Handlers, ready: boolean }
|
|
96
|
-
|
|
97
|
-
const BACKOFF_MS = [1000, 2000, 5000, 10000, 30000]
|
|
98
|
-
|
|
99
|
-
/** One multiplexed socket per server URL, shared by every channel of the app. */
|
|
100
|
-
class ChannelSocket {
|
|
101
|
-
private ws: WebSocket | null = null
|
|
102
|
-
private open = false
|
|
103
|
-
private subs = new Map<number, Sub>()
|
|
104
|
-
private nextId = 1
|
|
105
|
-
private attempts = 0
|
|
106
|
-
private timer: ReturnType<typeof setTimeout> | null = null
|
|
107
|
-
private closedByUs = false
|
|
108
|
-
private everOpened = false
|
|
109
|
-
private serverUrl: string
|
|
110
|
-
|
|
111
|
-
constructor(serverUrl: string) { this.serverUrl = serverUrl }
|
|
112
|
-
|
|
113
|
-
subscribe(ch: string, topic: string, handlers: Handlers): { close(): void } {
|
|
114
|
-
const sub: Sub = { id: this.nextId++, ch, topic, handlers, ready: false }
|
|
115
|
-
this.subs.set(sub.id, sub)
|
|
116
|
-
if (this.open) this.send({ t: "sub", id: sub.id, ch, topic })
|
|
117
|
-
else this.connect()
|
|
118
|
-
return {
|
|
119
|
-
close: () => {
|
|
120
|
-
if (!this.subs.delete(sub.id)) return
|
|
121
|
-
if (this.open) this.send({ t: "unsub", id: sub.id })
|
|
122
|
-
if (this.subs.size === 0) this.shutdown()
|
|
123
|
-
},
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
onSessionChanged(token: string | null) {
|
|
128
|
-
if (this.open) this.send({ t: "hello", session: token })
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
private connect() {
|
|
132
|
-
if (this.ws || this.timer !== null) return
|
|
133
|
-
this.closedByUs = false
|
|
134
|
-
const url = this.serverUrl.replace(/^http/, "ws") + "/ws"
|
|
135
|
-
const session = getSession(this.serverUrl)
|
|
136
|
-
const ws = new WebSocket(url, session ? { authorization: `Bearer ${session}` } : undefined)
|
|
137
|
-
this.ws = ws
|
|
138
|
-
ws.addEventListener("open", () => {
|
|
139
|
-
this.open = true
|
|
140
|
-
this.attempts = 0
|
|
141
|
-
this.send({ t: "hello", session: getSession(this.serverUrl) })
|
|
142
|
-
const reconnected = this.everOpened
|
|
143
|
-
this.everOpened = true
|
|
144
|
-
for (const sub of this.subs.values()) {
|
|
145
|
-
sub.ready = false
|
|
146
|
-
this.send({ t: "sub", id: sub.id, ch: sub.ch, topic: sub.topic })
|
|
147
|
-
if (reconnected) sub.handlers.reconnect?.()
|
|
148
|
-
}
|
|
149
|
-
})
|
|
150
|
-
ws.addEventListener("message", (data) => {
|
|
151
|
-
if (typeof data !== "string") return
|
|
152
|
-
let msg: any
|
|
153
|
-
try { msg = JSON.parse(data) } catch { return }
|
|
154
|
-
this.onFrame(msg)
|
|
155
|
-
})
|
|
156
|
-
const onDown = () => {
|
|
157
|
-
if (this.ws !== ws) return
|
|
158
|
-
this.ws = null
|
|
159
|
-
this.open = false
|
|
160
|
-
if (this.closedByUs || this.subs.size === 0) return
|
|
161
|
-
const delay = BACKOFF_MS[Math.min(this.attempts++, BACKOFF_MS.length - 1)]
|
|
162
|
-
this.timer = setTimeout(() => { this.timer = null; this.connect() }, delay)
|
|
163
|
-
}
|
|
164
|
-
ws.addEventListener("close", onDown)
|
|
165
|
-
ws.addEventListener("error", onDown)
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
private onFrame(msg: any) {
|
|
169
|
-
switch (msg?.t) {
|
|
170
|
-
case "session": setSession(this.serverUrl, msg.session ?? null); break
|
|
171
|
-
case "ok": { const s = this.subs.get(msg.id); if (s) s.ready = true; break }
|
|
172
|
-
case "err": {
|
|
173
|
-
const s = this.subs.get(msg.id)
|
|
174
|
-
if (!s) break
|
|
175
|
-
this.subs.delete(msg.id)
|
|
176
|
-
s.handlers.error?.(new RpcError(msg.status ?? 0, msg.message ?? "subscription refused"))
|
|
177
|
-
break
|
|
178
|
-
}
|
|
179
|
-
case "ev": {
|
|
180
|
-
for (const s of this.subs.values()) {
|
|
181
|
-
if (s.ch === msg.ch && s.topic === msg.topic) s.handlers[msg.event]?.(msg.data)
|
|
182
|
-
}
|
|
183
|
-
break
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
private send(frame: unknown) { this.ws?.send(JSON.stringify(frame)) }
|
|
189
|
-
|
|
190
|
-
private shutdown() {
|
|
191
|
-
this.closedByUs = true
|
|
192
|
-
if (this.timer !== null) { clearTimeout(this.timer); this.timer = null }
|
|
193
|
-
this.ws?.close()
|
|
194
|
-
this.ws = null
|
|
195
|
-
this.open = false
|
|
196
|
-
sockets.delete(this.serverUrl)
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const sockets = new Map<string, ChannelSocket>()
|
|
201
|
-
const socketFor = (serverUrl: string) => {
|
|
202
|
-
let s = sockets.get(serverUrl)
|
|
203
|
-
if (!s) { s = new ChannelSocket(serverUrl); sockets.set(serverUrl, s) }
|
|
204
|
-
return s
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
export interface ClientChannel<E extends Record<string, unknown> = Record<string, unknown>> {
|
|
208
|
-
/** Subscribe to `topic`; handlers are keyed by event name (+ `reconnect`/`error`). Returns `{ close }`. */
|
|
209
|
-
subscribe(topic: string, handlers: { [K in keyof E]?: (payload: E[K]) => void } & { reconnect?: () => void, error?: (e: RpcError) => void }): { close(): void }
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/** Build the client proxy of channel `id`. */
|
|
213
|
-
export const __channel = (serverUrl: string, id: string): ClientChannel<any> => ({
|
|
214
|
-
subscribe(topic, handlers) {
|
|
215
|
-
if (!serverUrl) throw noServer(id)
|
|
216
|
-
return socketFor(serverUrl).subscribe(id, topic, handlers as Handlers)
|
|
217
|
-
},
|
|
218
|
-
})
|