@crowdedkingdoms/crowdyjs 17.6.0 → 17.7.0-dev.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/MIGRATION.md +41 -0
- package/README.md +33 -0
- package/dist/client.d.ts +7 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +2 -1
- package/dist/crowdy-client.d.ts +24 -0
- package/dist/crowdy-client.d.ts.map +1 -1
- package/dist/crowdy-client.js +16 -0
- package/dist/crowdy-dsh/bridge.d.ts +24 -1
- package/dist/crowdy-dsh/bridge.d.ts.map +1 -1
- package/dist/crowdy-dsh/bridge.js +32 -0
- package/dist/crowdy-dsh/index.d.ts +1 -1
- package/dist/crowdy-dsh/index.d.ts.map +1 -1
- package/dist/crowdy-dsh/protocol.d.ts +64 -1
- package/dist/crowdy-dsh/protocol.d.ts.map +1 -1
- package/dist/crowdy-dsh/protocol.js +8 -1
- package/dist/crowdy-studio/starter-projects.js +1 -1
- package/dist/default-origin.d.ts +6 -6
- package/dist/default-origin.d.ts.map +1 -1
- package/dist/default-origin.js +6 -6
- package/dist/domains/actors.d.ts +38 -17
- package/dist/domains/actors.d.ts.map +1 -1
- package/dist/domains/actors.js +38 -17
- package/dist/domains/crowdyStudio.js +1 -1
- package/dist/domains/grids.d.ts +30 -0
- package/dist/domains/grids.d.ts.map +1 -0
- package/dist/domains/grids.js +39 -0
- package/dist/domains/udp.d.ts +5 -1
- package/dist/domains/udp.d.ts.map +1 -1
- package/dist/domains/udp.js +5 -1
- package/dist/generated/graphql.d.ts +169 -9
- package/dist/generated/graphql.d.ts.map +1 -1
- package/dist/generated/graphql.js +13 -10
- package/dist/grid-mods/grid-host-calls.d.ts +70 -0
- package/dist/grid-mods/grid-host-calls.d.ts.map +1 -0
- package/dist/grid-mods/grid-host-calls.js +220 -0
- package/dist/grid-mods/index.d.ts +3 -0
- package/dist/grid-mods/index.d.ts.map +1 -0
- package/dist/grid-mods/index.js +2 -0
- package/dist/grid-mods/mod-runtime.d.ts +52 -0
- package/dist/grid-mods/mod-runtime.d.ts.map +1 -0
- package/dist/grid-mods/mod-runtime.js +55 -0
- package/dist/grid-program/host.d.ts +88 -0
- package/dist/grid-program/host.d.ts.map +1 -0
- package/dist/grid-program/host.js +231 -0
- package/dist/grid-program/index.d.ts +14 -0
- package/dist/grid-program/index.d.ts.map +1 -0
- package/dist/grid-program/index.js +13 -0
- package/dist/grid-program/program.d.ts +19 -0
- package/dist/grid-program/program.d.ts.map +1 -0
- package/dist/grid-program/program.js +154 -0
- package/dist/grid-program/protocol.d.ts +87 -0
- package/dist/grid-program/protocol.d.ts.map +1 -0
- package/dist/grid-program/protocol.js +17 -0
- package/dist/grid-scope.d.ts +296 -0
- package/dist/grid-scope.d.ts.map +1 -0
- package/dist/grid-scope.js +183 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/kit/matches.d.ts +1 -0
- package/dist/kit/matches.d.ts.map +1 -1
- package/dist/kit/npcs.d.ts.map +1 -1
- package/dist/kit/social.d.ts.map +1 -1
- package/dist/live-coding/assets/browser-authoring-index.json +1 -1
- package/dist/live-coding/assets/manifest.json +1 -1
- package/dist/live-coding/browser-authoring-index.generated.d.ts.map +1 -1
- package/dist/live-coding/browser-authoring-index.generated.js +118 -6
- package/dist/player-runtime/client-grid-event-bus.d.ts +37 -0
- package/dist/player-runtime/client-grid-event-bus.d.ts.map +1 -0
- package/dist/player-runtime/client-grid-event-bus.js +56 -0
- package/dist/player-runtime/glue-runtime.d.ts +4 -2
- package/dist/player-runtime/glue-runtime.d.ts.map +1 -1
- package/dist/player-runtime/glue-runtime.js +20 -23
- package/dist/player-runtime/host-catalog.generated.d.ts +16 -0
- package/dist/player-runtime/host-catalog.generated.d.ts.map +1 -0
- package/dist/player-runtime/host-catalog.generated.js +296 -0
- package/dist/player-runtime/player-code-broker.d.ts +38 -0
- package/dist/player-runtime/player-code-broker.d.ts.map +1 -1
- package/dist/player-runtime/player-code-broker.js +116 -35
- package/dist/player-runtime/player-glue-worker.d.ts.map +1 -1
- package/dist/player-runtime/player-glue-worker.js +5 -0
- package/dist/realtime.d.ts +7 -0
- package/dist/realtime.d.ts.map +1 -1
- package/dist/realtime.js +3 -1
- package/package.json +10 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grid-host-calls.d.ts","sourceRoot":"","sources":["../../src/grid-mods/grid-host-calls.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAClF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAE5D;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,WAAW,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG;QAAE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;IACtF,aAAa,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAChF,QAAQ,CAAC,CAAC,KAAK,EAAE;QACf,KAAK,EAAE;YAAE,CAAC,EAAE,MAAM,CAAC;YAAC,CAAC,EAAE,MAAM,CAAC;YAAC,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QAC3C,CAAC,EAAE,MAAM,CAAC;QACV,CAAC,EAAE,MAAM,CAAC;QACV,CAAC,EAAE,MAAM,CAAC;QACV,SAAS,EAAE,MAAM,CAAC;QAClB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACrB,kBAAkB,CAAC,IAAI,OAAO,CAAC;CAChC;AAED,MAAM,WAAW,oBAAoB;IACnC,yEAAyE;IACzE,KAAK,EAAE,SAAS,CAAC;IACjB,wEAAwE;IACxE,MAAM,EAAE;QACN,MAAM,EAAE,SAAS,CAAC;QAClB,MAAM,EAAE,SAAS,CAAC;QAClB,KAAK,EAAE,QAAQ,CAAC;QAChB,SAAS,EAAE,YAAY,CAAC;KACzB,CAAC;IACF,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAClE,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,sEAAsE;AACtE,qBAAa,mBAAoB,SAAQ,KAAK;IAChC,QAAQ,CAAC,EAAE,EAAE,MAAM;gBAAV,EAAE,EAAE,MAAM,EAAE,MAAM,SAA+B;CAIvE;AAID;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,oBAAoB,GAC5B,CAAC,IAAI,EAAE,kBAAkB,KAAK,OAAO,CAAC,OAAO,CAAC,CA4KhD"}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/** Thrown for a host call this game does not offer in the browser. */
|
|
2
|
+
export class GridHostCallRefused extends Error {
|
|
3
|
+
constructor(fn, reason = 'not offered in the browser') {
|
|
4
|
+
super(`host call '${fn}' ${reason}`);
|
|
5
|
+
this.fn = fn;
|
|
6
|
+
this.name = 'GridHostCallRefused';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
const SPATIAL_KINDS = new Set(['actor', 'client_event', 'server_event', 'text']);
|
|
10
|
+
/**
|
|
11
|
+
* The page-side answer to every CLIENT host call in the platform catalog,
|
|
12
|
+
* through ordinary CrowdyJS confined to one grid (DN-10 §4). Hand the result
|
|
13
|
+
* to `PlayerCodeBroker({ onHostCall })`; the broker has already applied the
|
|
14
|
+
* allowlist, rate caps and chunk clamps, and answers `grid_info`, `emit_event`,
|
|
15
|
+
* `hud_set` and `overlay_draw` itself.
|
|
16
|
+
*/
|
|
17
|
+
export function createGridHostCalls(options) {
|
|
18
|
+
const { scope, client, local } = options;
|
|
19
|
+
const appId = scope.appId;
|
|
20
|
+
let gridChannels = null;
|
|
21
|
+
const ownChannels = () => (gridChannels ?? (gridChannels = scope.channels
|
|
22
|
+
.list()
|
|
23
|
+
.then((rows) => new Set(rows.map((row) => String(row.groupId))))
|
|
24
|
+
.catch(() => {
|
|
25
|
+
gridChannels = null;
|
|
26
|
+
return new Set();
|
|
27
|
+
})));
|
|
28
|
+
const channelAllowed = async (channelId) => options.channelFilter
|
|
29
|
+
? options.channelFilter(channelId)
|
|
30
|
+
: (await ownChannels()).has(channelId);
|
|
31
|
+
const uuid = (args) => typeof args.uuidHex === 'string' && /^[0-9a-f]{64}$/i.test(args.uuidHex)
|
|
32
|
+
? hexToAscii(args.uuidHex)
|
|
33
|
+
: (options.actorUuid ?? '0'.repeat(32));
|
|
34
|
+
return async ({ fn, args }) => {
|
|
35
|
+
switch (fn) {
|
|
36
|
+
case 'chunk_get': {
|
|
37
|
+
const [x, y, z] = coords(args.x, args.y, args.z);
|
|
38
|
+
scope.assertContains({ x, y, z });
|
|
39
|
+
const hit = local?.chunkVoxels?.(x, y, z);
|
|
40
|
+
if (hit !== undefined)
|
|
41
|
+
return { voxelsBase64: hit?.voxelsBase64 ?? null };
|
|
42
|
+
const chunk = await client.chunks.get({
|
|
43
|
+
appId,
|
|
44
|
+
coordinates: { x: x.toString(), y: y.toString(), z: z.toString() },
|
|
45
|
+
});
|
|
46
|
+
return { voxelsBase64: chunk?.voxels ?? null };
|
|
47
|
+
}
|
|
48
|
+
case 'voxels_list': {
|
|
49
|
+
const [x, y, z] = coords(args.x, args.y, args.z);
|
|
50
|
+
scope.assertContains({ x, y, z });
|
|
51
|
+
return {
|
|
52
|
+
voxels: await client.voxels.list({
|
|
53
|
+
appId,
|
|
54
|
+
coordinates: { x: x.toString(), y: y.toString(), z: z.toString() },
|
|
55
|
+
}),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
case 'actors_list':
|
|
59
|
+
case 'actors_list_radius': {
|
|
60
|
+
const [x, y, z] = coords(args.x, args.y, args.z);
|
|
61
|
+
scope.assertContains({ x, y, z });
|
|
62
|
+
if (!local?.actorsInChunk)
|
|
63
|
+
throw new GridHostCallRefused(fn);
|
|
64
|
+
// Same clamps as the server host: radiusXz <= 3, radiusY <= 1, box-clipped.
|
|
65
|
+
const rXz = fn === 'actors_list' ? 0n : BigInt(clampInt(args.radiusXz ?? args.radius, 0, 3));
|
|
66
|
+
const rY = fn === 'actors_list' ? 0n : BigInt(clampInt(args.radiusY, 0, 1));
|
|
67
|
+
const box = scope.bounds;
|
|
68
|
+
const actors = [];
|
|
69
|
+
for (let cx = max(box.low.x, x - rXz); cx <= min(box.high.x, x + rXz); cx++)
|
|
70
|
+
for (let cy = max(box.low.y, y - rY); cy <= min(box.high.y, y + rY); cy++)
|
|
71
|
+
for (let cz = max(box.low.z, z - rXz); cz <= min(box.high.z, z + rXz); cz++)
|
|
72
|
+
actors.push(...local.actorsInChunk(cx, cy, cz));
|
|
73
|
+
return { actors };
|
|
74
|
+
}
|
|
75
|
+
case 'voxel_set': {
|
|
76
|
+
const chunk = coords(args.chunkX, args.chunkY, args.chunkZ);
|
|
77
|
+
scope.assertContains({ x: chunk[0], y: chunk[1], z: chunk[2] });
|
|
78
|
+
const voxel = {
|
|
79
|
+
x: Number(args.voxelX ?? 0),
|
|
80
|
+
y: Number(args.voxelY ?? 0),
|
|
81
|
+
z: Number(args.voxelZ ?? 0),
|
|
82
|
+
};
|
|
83
|
+
const voxelType = Number(args.voxelType ?? 0);
|
|
84
|
+
const state = typeof args.stateBase64 === 'string' ? args.stateBase64 : undefined;
|
|
85
|
+
if (local?.setVoxel) {
|
|
86
|
+
return {
|
|
87
|
+
ok: await local.setVoxel({
|
|
88
|
+
chunk: { x: Number(chunk[0]), y: Number(chunk[1]), z: Number(chunk[2]) },
|
|
89
|
+
...voxel,
|
|
90
|
+
voxelType,
|
|
91
|
+
state,
|
|
92
|
+
}),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
await client.voxels.update({
|
|
96
|
+
appId,
|
|
97
|
+
coordinates: { x: chunk[0].toString(), y: chunk[1].toString(), z: chunk[2].toString() },
|
|
98
|
+
location: voxel,
|
|
99
|
+
voxelType,
|
|
100
|
+
...(state ? { state } : {}),
|
|
101
|
+
});
|
|
102
|
+
return { ok: true };
|
|
103
|
+
}
|
|
104
|
+
case 'emit_spatial': {
|
|
105
|
+
const kind = String(args.kind ?? '');
|
|
106
|
+
if (!SPATIAL_KINDS.has(kind))
|
|
107
|
+
throw new GridHostCallRefused(fn, 'has an unknown kind');
|
|
108
|
+
const [x, y, z] = coords(args.chunkX, args.chunkY, args.chunkZ);
|
|
109
|
+
const chunk = { x: x.toString(), y: y.toString(), z: z.toString() };
|
|
110
|
+
const payload = String(args.payloadBase64 ?? '');
|
|
111
|
+
const distance = Math.max(0, Math.min(8, Number(args.distance ?? 0) | 0));
|
|
112
|
+
const common = { chunk, uuid: uuid(args), distance };
|
|
113
|
+
if (kind === 'actor')
|
|
114
|
+
return scope.send.actorUpdate({ ...common, state: payload });
|
|
115
|
+
if (kind === 'text') {
|
|
116
|
+
return scope.send.text({ ...common, text: atob(payload) });
|
|
117
|
+
}
|
|
118
|
+
// client_event / server_event: [u16 eventType LE][state]
|
|
119
|
+
const bytes = base64Bytes(payload);
|
|
120
|
+
if (bytes.length < 2)
|
|
121
|
+
throw new GridHostCallRefused(fn, 'needs a uint16 eventType prefix');
|
|
122
|
+
return scope.send.clientEvent({
|
|
123
|
+
...common,
|
|
124
|
+
eventType: bytes[0] | (bytes[1] << 8),
|
|
125
|
+
state: bytesBase64(bytes.subarray(2)),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
case 'emit_channel': {
|
|
129
|
+
const channelId = String(args.channelId ?? '');
|
|
130
|
+
if (!(await channelAllowed(channelId))) {
|
|
131
|
+
throw new GridHostCallRefused(fn, 'targets a channel outside this grid');
|
|
132
|
+
}
|
|
133
|
+
return scope.channels.send(channelId, uuid(args), String(args.payloadBase64 ?? ''));
|
|
134
|
+
}
|
|
135
|
+
case 'container_create':
|
|
136
|
+
return scope.model.create({
|
|
137
|
+
typeKey: typeof args.typeName === 'string' ? args.typeName : undefined,
|
|
138
|
+
displayName: typeof args.displayName === 'string' ? args.displayName : undefined,
|
|
139
|
+
stateJson: JSON.stringify(args.state ?? args.properties ?? {}),
|
|
140
|
+
});
|
|
141
|
+
case 'container_get':
|
|
142
|
+
return scope.model.container(String(args.containerId));
|
|
143
|
+
case 'container_get_batch': {
|
|
144
|
+
const ids = Array.isArray(args.containerIds) ? args.containerIds.slice(0, 32) : [];
|
|
145
|
+
const found = await Promise.all(ids.map((id) => scope.model.container(String(id)).catch(() => null)));
|
|
146
|
+
return found.filter((c) => c != null);
|
|
147
|
+
}
|
|
148
|
+
case 'containers_list':
|
|
149
|
+
return scope.model.containers();
|
|
150
|
+
case 'container_delete':
|
|
151
|
+
return scope.model.delete(String(args.containerId));
|
|
152
|
+
case 'property_set':
|
|
153
|
+
return scope.model.set({
|
|
154
|
+
containerId: String(args.containerId),
|
|
155
|
+
propertyKey: String(args.key),
|
|
156
|
+
valueJson: JSON.stringify(args.value ?? null),
|
|
157
|
+
});
|
|
158
|
+
case 'model_invoke':
|
|
159
|
+
if (!options.allowModelInvoke)
|
|
160
|
+
throw new GridHostCallRefused(fn);
|
|
161
|
+
return client.gameModel.invoke({
|
|
162
|
+
appId,
|
|
163
|
+
functionName: String(args.functionName),
|
|
164
|
+
selfContainerId: String(args.selfContainerId),
|
|
165
|
+
paramsJson: JSON.stringify(args.params ?? {}),
|
|
166
|
+
...(typeof args.sessionId === 'string' ? { sessionId: args.sessionId } : {}),
|
|
167
|
+
});
|
|
168
|
+
case 'sessions_list':
|
|
169
|
+
return scope.sessions.list(typeof args.status === 'string' ? { status: args.status } : {});
|
|
170
|
+
case 'user_state_get':
|
|
171
|
+
return client.state.getOne(appId);
|
|
172
|
+
case 'user_state_set':
|
|
173
|
+
return client.state.update({
|
|
174
|
+
appId,
|
|
175
|
+
state: typeof args.stateBase64 === 'string' ? args.stateBase64 : null,
|
|
176
|
+
});
|
|
177
|
+
case 'pointer_clicks':
|
|
178
|
+
if (!local?.drainPointerClicks)
|
|
179
|
+
throw new GridHostCallRefused(fn);
|
|
180
|
+
return local.drainPointerClicks();
|
|
181
|
+
default:
|
|
182
|
+
// edge_*, grid_state_*, avatar_state_get and grid_permission_check have
|
|
183
|
+
// no browser GraphQL surface yet: the server host answers them for
|
|
184
|
+
// SERVER mods.
|
|
185
|
+
throw new GridHostCallRefused(fn);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function coords(x, y, z) {
|
|
190
|
+
return [BigInt(x), BigInt(y), BigInt(z)];
|
|
191
|
+
}
|
|
192
|
+
function clampInt(value, lo, hi) {
|
|
193
|
+
const n = Number(value ?? lo);
|
|
194
|
+
return Number.isFinite(n) ? Math.max(lo, Math.min(hi, Math.trunc(n))) : lo;
|
|
195
|
+
}
|
|
196
|
+
function min(a, b) {
|
|
197
|
+
return a < b ? a : b;
|
|
198
|
+
}
|
|
199
|
+
function max(a, b) {
|
|
200
|
+
return a > b ? a : b;
|
|
201
|
+
}
|
|
202
|
+
function hexToAscii(hex) {
|
|
203
|
+
let out = '';
|
|
204
|
+
for (let i = 0; i < hex.length; i += 2)
|
|
205
|
+
out += String.fromCharCode(parseInt(hex.slice(i, i + 2), 16));
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
function base64Bytes(b64) {
|
|
209
|
+
const bin = atob(b64);
|
|
210
|
+
const out = new Uint8Array(bin.length);
|
|
211
|
+
for (let i = 0; i < bin.length; i++)
|
|
212
|
+
out[i] = bin.charCodeAt(i);
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
function bytesBase64(bytes) {
|
|
216
|
+
let bin = '';
|
|
217
|
+
for (let i = 0; i < bytes.length; i++)
|
|
218
|
+
bin += String.fromCharCode(bytes[i]);
|
|
219
|
+
return btoa(bin);
|
|
220
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { createGridHostCalls, GridHostCallRefused, type GridHostCallsOptions, type GridHostLocal, } from './grid-host-calls.js';
|
|
2
|
+
export { startGridMod, type GridModSpec, type WasmGridModSpec, type ProgramGridModSpec, type RunningGridMod, type StartGridModOptions, } from './mod-runtime.js';
|
|
3
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/grid-mods/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,KAAK,oBAAoB,EACzB,KAAK,aAAa,GACnB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,YAAY,EACZ,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,mBAAmB,GACzB,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type PlayerCodePresentation } from '../player-runtime/player-code-broker.js';
|
|
2
|
+
import type { GridProgramPort } from '../grid-program/protocol.js';
|
|
3
|
+
import type { GridScope } from '../grid-scope.js';
|
|
4
|
+
import { type GridHostCallsOptions, type GridHostLocal } from './grid-host-calls.js';
|
|
5
|
+
/** A Rust CLIENT mod: platform-built WASM run in the tokenless glue worker. */
|
|
6
|
+
export interface WasmGridModSpec {
|
|
7
|
+
kind: 'wasm';
|
|
8
|
+
moduleName: string;
|
|
9
|
+
artifact: ArrayBuffer;
|
|
10
|
+
artifactHash?: string;
|
|
11
|
+
fuelPerDispatch?: bigint;
|
|
12
|
+
tickIntervalMs?: number;
|
|
13
|
+
/** The platform glue worker URL (`@crowdedkingdoms/crowdyjs/player-glue-worker`). */
|
|
14
|
+
workerUrl: string | URL;
|
|
15
|
+
}
|
|
16
|
+
/** A JS grid program: player code on the far side of a MessagePort. */
|
|
17
|
+
export interface ProgramGridModSpec {
|
|
18
|
+
kind: 'program';
|
|
19
|
+
moduleName: string;
|
|
20
|
+
/** The page's end of the channel whose other end the sandbox holds. */
|
|
21
|
+
port: GridProgramPort;
|
|
22
|
+
ttlSeconds?: number;
|
|
23
|
+
}
|
|
24
|
+
export type GridModSpec = WasmGridModSpec | ProgramGridModSpec;
|
|
25
|
+
export interface StartGridModOptions {
|
|
26
|
+
spec: GridModSpec;
|
|
27
|
+
/** The grid, with its box (`client.grid(...)` after `mintToken()`, or passed in). */
|
|
28
|
+
scope: GridScope;
|
|
29
|
+
/** The page's CrowdyJS client (holds the player's app token). */
|
|
30
|
+
client: GridHostCallsOptions['client'];
|
|
31
|
+
/** ck-api endpoints, for grid programs. */
|
|
32
|
+
graphqlUrl?: string;
|
|
33
|
+
graphqlWsUrl?: string;
|
|
34
|
+
local?: GridHostLocal;
|
|
35
|
+
allowModelInvoke?: boolean;
|
|
36
|
+
onPresentation?: (presentation: PlayerCodePresentation) => void;
|
|
37
|
+
onStopped?: (reason: string) => void;
|
|
38
|
+
}
|
|
39
|
+
export interface RunningGridMod {
|
|
40
|
+
kind: GridModSpec['kind'];
|
|
41
|
+
moduleName: string;
|
|
42
|
+
stop(): void;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* One way to run player code inside a grid, whatever it is written in (DN-10
|
|
46
|
+
* §4-5). Rust CLIENT mods get the full client host catalog through
|
|
47
|
+
* {@link createGridHostCalls}; JS grid programs get real CrowdyJS through a
|
|
48
|
+
* grid-token relay. Both are confined to `scope`'s grid, locally for UX and by
|
|
49
|
+
* the server for authority.
|
|
50
|
+
*/
|
|
51
|
+
export declare function startGridMod(options: StartGridModOptions): Promise<RunningGridMod>;
|
|
52
|
+
//# sourceMappingURL=mod-runtime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mod-runtime.d.ts","sourceRoot":"","sources":["../../src/grid-mods/mod-runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,sBAAsB,EAC5B,MAAM,yCAAyC,CAAC;AAEjD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAEL,KAAK,oBAAoB,EACzB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAE9B,+EAA+E;AAC/E,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,WAAW,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qFAAqF;IACrF,SAAS,EAAE,MAAM,GAAG,GAAG,CAAC;CACzB;AAED,uEAAuE;AACvE,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,SAAS,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,IAAI,EAAE,eAAe,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,MAAM,WAAW,GAAG,eAAe,GAAG,kBAAkB,CAAC;AAE/D,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,WAAW,CAAC;IAClB,qFAAqF;IACrF,KAAK,EAAE,SAAS,CAAC;IACjB,iEAAiE;IACjE,MAAM,EAAE,oBAAoB,CAAC,QAAQ,CAAC,CAAC;IACvC,2CAA2C;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,cAAc,CAAC,EAAE,CAAC,YAAY,EAAE,sBAAsB,KAAK,IAAI,CAAC;IAChE,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,IAAI,IAAI,CAAC;CACd;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAChC,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,cAAc,CAAC,CA2CzB"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { PlayerCodeBroker, } from '../player-runtime/player-code-broker.js';
|
|
2
|
+
import { hostGridProgram } from '../grid-program/host.js';
|
|
3
|
+
import { createGridHostCalls, } from './grid-host-calls.js';
|
|
4
|
+
/**
|
|
5
|
+
* One way to run player code inside a grid, whatever it is written in (DN-10
|
|
6
|
+
* §4-5). Rust CLIENT mods get the full client host catalog through
|
|
7
|
+
* {@link createGridHostCalls}; JS grid programs get real CrowdyJS through a
|
|
8
|
+
* grid-token relay. Both are confined to `scope`'s grid, locally for UX and by
|
|
9
|
+
* the server for authority.
|
|
10
|
+
*/
|
|
11
|
+
export async function startGridMod(options) {
|
|
12
|
+
const { spec, scope } = options;
|
|
13
|
+
if (!scope.bounds)
|
|
14
|
+
await scope.mintToken();
|
|
15
|
+
const box = scope.bounds;
|
|
16
|
+
if (spec.kind === 'wasm') {
|
|
17
|
+
const broker = new PlayerCodeBroker({
|
|
18
|
+
workerUrl: spec.workerUrl,
|
|
19
|
+
grid: { low: box.low, high: box.high, gridId: scope.gridId },
|
|
20
|
+
moduleName: spec.moduleName,
|
|
21
|
+
artifactHash: spec.artifactHash,
|
|
22
|
+
fuelPerDispatch: spec.fuelPerDispatch,
|
|
23
|
+
tickIntervalMs: spec.tickIntervalMs,
|
|
24
|
+
onHostCall: createGridHostCalls({
|
|
25
|
+
scope,
|
|
26
|
+
client: options.client,
|
|
27
|
+
local: options.local,
|
|
28
|
+
allowModelInvoke: options.allowModelInvoke,
|
|
29
|
+
}),
|
|
30
|
+
onPresentation: options.onPresentation,
|
|
31
|
+
onCircuitOpen: (reason) => options.onStopped?.(reason),
|
|
32
|
+
});
|
|
33
|
+
await broker.start(spec.artifact);
|
|
34
|
+
return {
|
|
35
|
+
kind: 'wasm',
|
|
36
|
+
moduleName: spec.moduleName,
|
|
37
|
+
stop: () => broker.stop(),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (!options.graphqlUrl || !options.graphqlWsUrl) {
|
|
41
|
+
throw new Error('a grid program needs graphqlUrl and graphqlWsUrl');
|
|
42
|
+
}
|
|
43
|
+
const host = await hostGridProgram({
|
|
44
|
+
port: spec.port,
|
|
45
|
+
scope,
|
|
46
|
+
graphqlUrl: options.graphqlUrl,
|
|
47
|
+
graphqlWsUrl: options.graphqlWsUrl,
|
|
48
|
+
ttlSeconds: spec.ttlSeconds,
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
kind: 'program',
|
|
52
|
+
moduleName: spec.moduleName,
|
|
53
|
+
stop: () => host.stop(),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { type GridProgramPort } from './protocol.js';
|
|
2
|
+
import type { GridScope } from '../grid-scope.js';
|
|
3
|
+
/** A grid token as the host holds it. */
|
|
4
|
+
export interface HeldGridToken {
|
|
5
|
+
token: string;
|
|
6
|
+
expiresAt: string | Date;
|
|
7
|
+
low: {
|
|
8
|
+
x: string;
|
|
9
|
+
y: string;
|
|
10
|
+
z: string;
|
|
11
|
+
};
|
|
12
|
+
high: {
|
|
13
|
+
x: string;
|
|
14
|
+
y: string;
|
|
15
|
+
z: string;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export interface GridProgramHostOptions {
|
|
19
|
+
/** The port to the program (the iframe's side of a MessageChannel). */
|
|
20
|
+
port: GridProgramPort;
|
|
21
|
+
appId: string;
|
|
22
|
+
gridId: string;
|
|
23
|
+
/** ck-api GraphQL HTTP endpoint (…/graphql). */
|
|
24
|
+
graphqlUrl: string;
|
|
25
|
+
/** ck-api GraphQL WebSocket endpoint (ws…/graphql). */
|
|
26
|
+
graphqlWsUrl: string;
|
|
27
|
+
/**
|
|
28
|
+
* Mint a grid token. Usually `() => scope.mintToken()` on the PAGE's client,
|
|
29
|
+
* which holds the player's app token; see {@link hostGridProgram}.
|
|
30
|
+
*/
|
|
31
|
+
mintToken: () => Promise<HeldGridToken>;
|
|
32
|
+
/** Defaults to the global fetch / WebSocket. */
|
|
33
|
+
fetchImpl?: typeof fetch;
|
|
34
|
+
WebSocketImpl?: new (url: string, protocols?: string | string[]) => WebSocket;
|
|
35
|
+
/** Largest request body relayed (default 256 KiB). */
|
|
36
|
+
maxBodyBytes?: number;
|
|
37
|
+
/** Requests in flight at once (default 16). */
|
|
38
|
+
maxInFlight?: number;
|
|
39
|
+
/** WebSocket frames relayed per second per program (default 200). */
|
|
40
|
+
maxFramesPerSecond?: number;
|
|
41
|
+
/** Diagnostics for refusals (never receives the token). */
|
|
42
|
+
onRefused?: (reason: string) => void;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The page side of a grid program. It relays the program's CrowdyJS traffic
|
|
46
|
+
* to ck-api, attaching a GRID-SCOPED token the program never sees, so the
|
|
47
|
+
* server confines everything the program does to the grid (DN-10 §5). Local
|
|
48
|
+
* checks here are cheap refusals of obviously wrong traffic; the server is
|
|
49
|
+
* the authority.
|
|
50
|
+
*/
|
|
51
|
+
export declare class GridProgramHost {
|
|
52
|
+
private readonly options;
|
|
53
|
+
private held;
|
|
54
|
+
private minting;
|
|
55
|
+
private inFlight;
|
|
56
|
+
private readonly sockets;
|
|
57
|
+
private frameWindowStart;
|
|
58
|
+
private framesInWindow;
|
|
59
|
+
private stopped;
|
|
60
|
+
private readonly listener;
|
|
61
|
+
constructor(options: GridProgramHostOptions);
|
|
62
|
+
/** Mint the first token, announce the grid to the program, start relaying. */
|
|
63
|
+
start(): Promise<void>;
|
|
64
|
+
stop(): void;
|
|
65
|
+
private token;
|
|
66
|
+
private reply;
|
|
67
|
+
private refuse;
|
|
68
|
+
private onRequest;
|
|
69
|
+
private relayHttp;
|
|
70
|
+
private openSocket;
|
|
71
|
+
private relayFrame;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Host a grid program for one grid from the page's own client. Mints grid
|
|
75
|
+
* tokens through `scope.mintToken()` (the page holds the app token) and
|
|
76
|
+
* relays the program's traffic; returns the running host.
|
|
77
|
+
*/
|
|
78
|
+
export declare function hostGridProgram(options: {
|
|
79
|
+
port: GridProgramPort;
|
|
80
|
+
scope: GridScope;
|
|
81
|
+
graphqlUrl: string;
|
|
82
|
+
graphqlWsUrl: string;
|
|
83
|
+
ttlSeconds?: number;
|
|
84
|
+
onRefused?: (reason: string) => void;
|
|
85
|
+
fetchImpl?: typeof fetch;
|
|
86
|
+
WebSocketImpl?: GridProgramHostOptions['WebSocketImpl'];
|
|
87
|
+
}): Promise<GridProgramHost>;
|
|
88
|
+
//# sourceMappingURL=host.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../../src/grid-program/host.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,eAAe,EAGrB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAElD,yCAAyC;AACzC,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,GAAG,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,IAAI,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3C;AAED,MAAM,WAAW,sBAAsB;IACrC,uEAAuE;IACvE,IAAI,EAAE,eAAe,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,gDAAgD;IAChD,UAAU,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,YAAY,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,SAAS,EAAE,MAAM,OAAO,CAAC,aAAa,CAAC,CAAC;IACxC,gDAAgD;IAChD,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,aAAa,CAAC,EAAE,KAAK,GAAG,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,KAAK,SAAS,CAAC;IAC9E,sDAAsD;IACtD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,+CAA+C;IAC/C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qEAAqE;IACrE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,2DAA2D;IAC3D,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC;AAID;;;;;;GAMG;AACH,qBAAa,eAAe;IAYd,OAAO,CAAC,QAAQ,CAAC,OAAO;IAXpC,OAAO,CAAC,IAAI,CAA8B;IAC1C,OAAO,CAAC,OAAO,CAAuC;IACtD,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgC;IACxD,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAEvB;gBAE2B,OAAO,EAAE,sBAAsB;IAE5D,8EAA8E;IACxE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAc5B,IAAI,IAAI,IAAI;YAOE,KAAK;IAYnB,OAAO,CAAC,KAAK;IAKb,OAAO,CAAC,MAAM;YAIA,SAAS;YAkBT,SAAS;YA4DT,UAAU;YA+BV,UAAU;CAqCzB;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE;IAC7C,IAAI,EAAE,eAAe,CAAC;IACtB,KAAK,EAAE,SAAS,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,aAAa,CAAC,EAAE,sBAAsB,CAAC,eAAe,CAAC,CAAC;CACzD,GAAG,OAAO,CAAC,eAAe,CAAC,CAsB3B"}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { parse } from 'graphql';
|
|
2
|
+
import { GRID_PROGRAM_PROTOCOL_VERSION, GRID_PROGRAM_WS_PROTOCOL, } from './protocol.js';
|
|
3
|
+
const REFRESH_MARGIN_MS = 60000;
|
|
4
|
+
/**
|
|
5
|
+
* The page side of a grid program. It relays the program's CrowdyJS traffic
|
|
6
|
+
* to ck-api, attaching a GRID-SCOPED token the program never sees, so the
|
|
7
|
+
* server confines everything the program does to the grid (DN-10 §5). Local
|
|
8
|
+
* checks here are cheap refusals of obviously wrong traffic; the server is
|
|
9
|
+
* the authority.
|
|
10
|
+
*/
|
|
11
|
+
export class GridProgramHost {
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.options = options;
|
|
14
|
+
this.held = null;
|
|
15
|
+
this.minting = null;
|
|
16
|
+
this.inFlight = 0;
|
|
17
|
+
this.sockets = new Map();
|
|
18
|
+
this.frameWindowStart = 0;
|
|
19
|
+
this.framesInWindow = 0;
|
|
20
|
+
this.stopped = false;
|
|
21
|
+
this.listener = (event) => {
|
|
22
|
+
void this.onRequest(event.data);
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** Mint the first token, announce the grid to the program, start relaying. */
|
|
26
|
+
async start() {
|
|
27
|
+
const held = await this.token();
|
|
28
|
+
this.options.port.addEventListener('message', this.listener);
|
|
29
|
+
this.options.port.start?.();
|
|
30
|
+
this.reply({
|
|
31
|
+
t: 'hello',
|
|
32
|
+
v: GRID_PROGRAM_PROTOCOL_VERSION,
|
|
33
|
+
appId: this.options.appId,
|
|
34
|
+
gridId: this.options.gridId,
|
|
35
|
+
low: held.low,
|
|
36
|
+
high: held.high,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
stop() {
|
|
40
|
+
this.stopped = true;
|
|
41
|
+
this.options.port.removeEventListener('message', this.listener);
|
|
42
|
+
for (const socket of this.sockets.values())
|
|
43
|
+
socket.close(1000, 'host stopped');
|
|
44
|
+
this.sockets.clear();
|
|
45
|
+
}
|
|
46
|
+
async token() {
|
|
47
|
+
const held = this.held;
|
|
48
|
+
if (held && new Date(held.expiresAt).getTime() - Date.now() > REFRESH_MARGIN_MS) {
|
|
49
|
+
return held;
|
|
50
|
+
}
|
|
51
|
+
this.minting ?? (this.minting = this.options.mintToken().finally(() => {
|
|
52
|
+
this.minting = null;
|
|
53
|
+
}));
|
|
54
|
+
this.held = await this.minting;
|
|
55
|
+
return this.held;
|
|
56
|
+
}
|
|
57
|
+
reply(message) {
|
|
58
|
+
if (this.stopped)
|
|
59
|
+
return;
|
|
60
|
+
this.options.port.postMessage(message);
|
|
61
|
+
}
|
|
62
|
+
refuse(reason) {
|
|
63
|
+
this.options.onRefused?.(reason);
|
|
64
|
+
}
|
|
65
|
+
async onRequest(raw) {
|
|
66
|
+
if (this.stopped || !raw || typeof raw !== 'object')
|
|
67
|
+
return;
|
|
68
|
+
const msg = raw;
|
|
69
|
+
switch (msg.t) {
|
|
70
|
+
case 'http':
|
|
71
|
+
return this.relayHttp(msg);
|
|
72
|
+
case 'ws-open':
|
|
73
|
+
return this.openSocket(msg);
|
|
74
|
+
case 'ws-send':
|
|
75
|
+
return this.relayFrame(msg);
|
|
76
|
+
case 'ws-close':
|
|
77
|
+
this.sockets.get(msg.sid)?.close(msg.code ?? 1000, msg.reason ?? '');
|
|
78
|
+
return;
|
|
79
|
+
default:
|
|
80
|
+
this.refuse('unknown message');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async relayHttp(msg) {
|
|
84
|
+
const fail = (message) => {
|
|
85
|
+
this.refuse(message);
|
|
86
|
+
this.reply({ t: 'http-error', id: msg.id, message });
|
|
87
|
+
};
|
|
88
|
+
if (!Number.isSafeInteger(msg.id) || typeof msg.body !== 'string') {
|
|
89
|
+
return fail('malformed request');
|
|
90
|
+
}
|
|
91
|
+
if (msg.body.length > (this.options.maxBodyBytes ?? 256 * 1024)) {
|
|
92
|
+
return fail('request body too large');
|
|
93
|
+
}
|
|
94
|
+
if (this.inFlight >= (this.options.maxInFlight ?? 16)) {
|
|
95
|
+
return fail('too many requests in flight');
|
|
96
|
+
}
|
|
97
|
+
let body;
|
|
98
|
+
try {
|
|
99
|
+
body = JSON.parse(msg.body);
|
|
100
|
+
if (typeof body.query !== 'string')
|
|
101
|
+
throw new Error('no query');
|
|
102
|
+
const ops = parse(body.query).definitions.filter((d) => d.kind === 'OperationDefinition');
|
|
103
|
+
if (ops.length !== 1 || ops[0].operation === 'subscription') {
|
|
104
|
+
throw new Error('one query or mutation per request');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
return fail(`refused: ${error.message}`);
|
|
109
|
+
}
|
|
110
|
+
this.inFlight += 1;
|
|
111
|
+
try {
|
|
112
|
+
const held = await this.token();
|
|
113
|
+
const response = await (this.options.fetchImpl ?? fetch)(this.options.graphqlUrl, {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers: {
|
|
116
|
+
'content-type': 'application/json',
|
|
117
|
+
authorization: `Bearer ${held.token}`,
|
|
118
|
+
},
|
|
119
|
+
body: JSON.stringify({
|
|
120
|
+
query: body.query,
|
|
121
|
+
variables: body.variables ?? {},
|
|
122
|
+
...(typeof body.operationName === 'string'
|
|
123
|
+
? { operationName: body.operationName }
|
|
124
|
+
: {}),
|
|
125
|
+
}),
|
|
126
|
+
});
|
|
127
|
+
this.reply({
|
|
128
|
+
t: 'http-result',
|
|
129
|
+
id: msg.id,
|
|
130
|
+
status: response.status,
|
|
131
|
+
body: await response.text(),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
this.reply({ t: 'http-error', id: msg.id, message: error.message });
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
this.inFlight -= 1;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async openSocket(msg) {
|
|
142
|
+
if (!Number.isSafeInteger(msg.sid) ||
|
|
143
|
+
this.sockets.has(msg.sid) ||
|
|
144
|
+
!Array.isArray(msg.protocols) ||
|
|
145
|
+
!msg.protocols.includes(GRID_PROGRAM_WS_PROTOCOL)) {
|
|
146
|
+
this.refuse('websocket open refused');
|
|
147
|
+
this.reply({ t: 'ws-closed', sid: msg.sid, code: 4400, reason: 'refused' });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const Impl = this.options.WebSocketImpl ?? WebSocket;
|
|
151
|
+
const socket = new Impl(this.options.graphqlWsUrl, GRID_PROGRAM_WS_PROTOCOL);
|
|
152
|
+
this.sockets.set(msg.sid, socket);
|
|
153
|
+
socket.onopen = () => this.reply({ t: 'ws-opened', sid: msg.sid, protocol: socket.protocol });
|
|
154
|
+
socket.onmessage = (event) => this.reply({ t: 'ws-message', sid: msg.sid, data: String(event.data) });
|
|
155
|
+
socket.onerror = () => this.reply({ t: 'ws-error', sid: msg.sid, message: 'websocket error' });
|
|
156
|
+
socket.onclose = (event) => {
|
|
157
|
+
this.sockets.delete(msg.sid);
|
|
158
|
+
this.reply({
|
|
159
|
+
t: 'ws-closed',
|
|
160
|
+
sid: msg.sid,
|
|
161
|
+
code: event.code,
|
|
162
|
+
reason: event.reason,
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
async relayFrame(msg) {
|
|
167
|
+
const socket = this.sockets.get(msg.sid);
|
|
168
|
+
if (!socket || typeof msg.data !== 'string')
|
|
169
|
+
return;
|
|
170
|
+
const now = Date.now();
|
|
171
|
+
if (now - this.frameWindowStart >= 1000) {
|
|
172
|
+
this.frameWindowStart = now;
|
|
173
|
+
this.framesInWindow = 0;
|
|
174
|
+
}
|
|
175
|
+
if (++this.framesInWindow > (this.options.maxFramesPerSecond ?? 200)) {
|
|
176
|
+
this.refuse('websocket frame rate exceeded');
|
|
177
|
+
socket.close(4429, 'rate limited');
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
let frame;
|
|
181
|
+
try {
|
|
182
|
+
frame = JSON.parse(msg.data);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
this.refuse('malformed websocket frame');
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (frame.type === 'connection_init') {
|
|
189
|
+
// The program's credentials are a placeholder: swap in the grid token.
|
|
190
|
+
const held = await this.token();
|
|
191
|
+
socket.send(JSON.stringify({
|
|
192
|
+
type: 'connection_init',
|
|
193
|
+
payload: {
|
|
194
|
+
Authorization: `Bearer ${held.token}`,
|
|
195
|
+
appId: this.options.appId,
|
|
196
|
+
clientKind: 'grid-program',
|
|
197
|
+
},
|
|
198
|
+
}));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
socket.send(msg.data);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Host a grid program for one grid from the page's own client. Mints grid
|
|
206
|
+
* tokens through `scope.mintToken()` (the page holds the app token) and
|
|
207
|
+
* relays the program's traffic; returns the running host.
|
|
208
|
+
*/
|
|
209
|
+
export async function hostGridProgram(options) {
|
|
210
|
+
const host = new GridProgramHost({
|
|
211
|
+
port: options.port,
|
|
212
|
+
appId: options.scope.appId,
|
|
213
|
+
gridId: options.scope.gridId,
|
|
214
|
+
graphqlUrl: options.graphqlUrl,
|
|
215
|
+
graphqlWsUrl: options.graphqlWsUrl,
|
|
216
|
+
onRefused: options.onRefused,
|
|
217
|
+
fetchImpl: options.fetchImpl,
|
|
218
|
+
WebSocketImpl: options.WebSocketImpl,
|
|
219
|
+
mintToken: async () => {
|
|
220
|
+
const minted = await options.scope.mintToken(options.ttlSeconds);
|
|
221
|
+
return {
|
|
222
|
+
token: minted.token,
|
|
223
|
+
expiresAt: minted.expiresAt,
|
|
224
|
+
low: minted.lowChunk,
|
|
225
|
+
high: minted.highChunk,
|
|
226
|
+
};
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
await host.start();
|
|
230
|
+
return host;
|
|
231
|
+
}
|