dewdrop-sdk 0.5.2 → 0.5.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dewdrop-sdk",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "Build a three.js game for Dewdrop: the Vite plugin, model calls, multiplayer, saves and voice.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/backend.js CHANGED
@@ -7,7 +7,7 @@
7
7
  // past its cap. The page's URL says which room the tab joins and under
8
8
  // what name; on Dewdrop the game's page says which room, and the name is
9
9
  // the player's username.
10
- import { MAX_NET_CLIENTS, MAX_NET_CLIENT_FRAME_BYTES, createInbox, createOutbox, defineHandlers, label, side, unpack } from './net.js';
10
+ import { DEFAULT_ROOM_PLAYERS, MAX_NET_CLIENTS, MAX_NET_CLIENT_FRAME_BYTES, createInbox, createOutbox, defineHandlers, label, side, unpack } from './net.js';
11
11
  import { NO_CHANNEL } from './route.js';
12
12
 
13
13
  let channel = null;
@@ -59,9 +59,9 @@ function name(text) {
59
59
  }
60
60
 
61
61
  // The players a room seats as `?players=` names them: a whole number from
62
- // 1 to MAX_NET_CLIENTS, or the most without one or with any other.
62
+ // 1 to MAX_NET_CLIENTS, or DEFAULT_ROOM_PLAYERS without one or with any other.
63
63
  function players(text) {
64
- return /^[0-9]{1,3}$/.test(text) && Number(text) >= 1 && Number(text) <= MAX_NET_CLIENTS ? Number(text) : MAX_NET_CLIENTS;
64
+ return /^[0-9]{1,3}$/.test(text) && Number(text) >= 1 && Number(text) <= MAX_NET_CLIENTS ? Number(text) : DEFAULT_ROOM_PLAYERS;
65
65
  }
66
66
 
67
67
  // Where a page's URL joins its tab: the room `?room=` names, the name
@@ -86,7 +86,7 @@ export function gameHasBackend() {
86
86
  // function at the next animation frame. Without a plugin there is no
87
87
  // channel, and `send` says so. A room that seats no more says so in the
88
88
  // console, as Dewdrop refuses the connect, and the tab's sends reach no one.
89
- export function attachBackend(hot, { channel: routed, hasBackend: present, frame: schedule, room = '', player = '', players = MAX_NET_CLIENTS }) {
89
+ export function attachBackend(hot, { channel: routed, hasBackend: present, frame: schedule, room = '', player = '', players = DEFAULT_ROOM_PLAYERS }) {
90
90
  if (!routed) return;
91
91
  channel = hot;
92
92
  hasBackend = present;
@@ -0,0 +1,57 @@
1
+ // A page the dev server serves offers `dewdrop.evaluate(code)`, as a dev
2
+ // version's page on Dewdrop does, so one script drives the game in both.
3
+ // `code`, a string or a function sent as its source text called with
4
+ // nothing, runs at the page's top level, as the browser's console runs
5
+ // what is typed into it: what the game puts on `globalThis` is in reach and
6
+ // its modules' own bindings are not. The call resolves with the value the
7
+ // code came to as JSON carries it, a promise's once it settles, `undefined`
8
+ // for none, or rejects with an Error carrying the text of what the code
9
+ // threw; code, waiting calls and values past what Dewdrop takes are
10
+ // refused in Dewdrop's words. The plugin puts `evaluateScript` in the
11
+ // page's head; it is `offerEvaluate`'s own text, so the function imports
12
+ // nothing and is written without a `<`.
13
+
14
+ // What Dewdrop takes: the bytes of a call's code, the calls waiting at
15
+ // once, and the bytes of a value's JSON.
16
+ export const LIMITS = Object.freeze({ code: 32764, waiting: 16, value: 65530 });
17
+
18
+ // Offers `dewdrop.evaluate` on `scope`, the page's window, within `limits`.
19
+ // Assigned rather than defined, since the game shares the window here.
20
+ export function offerEvaluate(scope, limits) {
21
+ const run = scope.eval;
22
+ const encoder = new scope.TextEncoder();
23
+ const describe = (error) => {
24
+ try {
25
+ return error !== null && typeof error === 'object' && typeof error.stack === 'string' ? error.stack : String(error);
26
+ } catch {
27
+ return '[unprintable]';
28
+ }
29
+ };
30
+ let waiting = 0;
31
+ async function evaluate(code) {
32
+ if (typeof code === 'function') code = `(${code})()`;
33
+ else if (typeof code !== 'string') throw new TypeError('dewdrop.evaluate takes a string or a function');
34
+ if (waiting >= limits.waiting) throw new RangeError(`dewdrop.evaluate has ${limits.waiting} calls waiting already`);
35
+ const bytes = encoder.encode(code).length;
36
+ if (bytes > limits.code) throw new RangeError(`dewdrop.evaluate takes code of at most ${limits.code} bytes, not ${bytes}`);
37
+ waiting += 1;
38
+ let text;
39
+ try {
40
+ text = JSON.stringify(await run(code));
41
+ } catch (error) {
42
+ throw new Error(describe(error));
43
+ } finally {
44
+ waiting -= 1;
45
+ }
46
+ if (text === undefined) return undefined;
47
+ const length = encoder.encode(text).length;
48
+ if (length > limits.value) throw new Error(`the value is ${length} bytes as JSON, more than the ${limits.value} an answer carries`);
49
+ return JSON.parse(text);
50
+ }
51
+ scope.dewdrop = Object.freeze({ evaluate });
52
+ }
53
+
54
+ // The script the plugin puts in a page the dev server serves.
55
+ export function evaluateScript() {
56
+ return `(${offerEvaluate})(window, ${JSON.stringify(LIMITS)});`;
57
+ }
package/src/net.js CHANGED
@@ -18,6 +18,9 @@ export const MAX_NET_PLAYER_BYTES = 64;
18
18
  // The most players a room seats, and the most clients it holds: a room's
19
19
  // size is 1 to this.
20
20
  export const MAX_NET_CLIENTS = 100;
21
+ // The players a room seats when its page names no size: the most a game's
22
+ // room seats on Dewdrop until its developer says otherwise.
23
+ export const DEFAULT_ROOM_PLAYERS = 8;
21
24
  // How often the local backend ticks: sixty times a second.
22
25
  export const NET_TICK_MS = 1000 / 60;
23
26
 
package/src/serve.js CHANGED
@@ -17,7 +17,7 @@
17
17
  import { existsSync, readFileSync } from 'node:fs';
18
18
  import { dirname, resolve } from 'node:path';
19
19
  import { Worker } from 'node:worker_threads';
20
- import { MAX_NET_CLIENTS, label } from './net.js';
20
+ import { DEFAULT_ROOM_PLAYERS, MAX_NET_CLIENTS, label } from './net.js';
21
21
 
22
22
  // How long a room's backend runs on after its last tab's leave, as on
23
23
  // Dewdrop.
@@ -174,13 +174,13 @@ export function createBackendHost({ entry, ws, watcher, origin = () => '', log =
174
174
  // A tab joins the room its page named under the name it gave, a room
175
175
  // it makes seating the players its page said; a page that named
176
176
  // neither joins the empty room unnamed, and one that named no size a
177
- // room seating the most.
178
- join(client, { room = '', player = '', players = MAX_NET_CLIENTS } = {}) {
177
+ // room seating DEFAULT_ROOM_PLAYERS, as a game's rooms on Dewdrop do.
178
+ join(client, { room = '', player = '', players = DEFAULT_ROOM_PLAYERS } = {}) {
179
179
  if (tabs.has(client)) return;
180
180
  const key = typeof room === 'string' ? room : '';
181
181
  let it = rooms.get(key);
182
182
  if (it === undefined) {
183
- const capacity = Number.isInteger(players) && players >= 1 && players <= MAX_NET_CLIENTS ? players : MAX_NET_CLIENTS;
183
+ const capacity = Number.isInteger(players) && players >= 1 && players <= MAX_NET_CLIENTS ? players : DEFAULT_ROOM_PLAYERS;
184
184
  it = createRoom(key, capacity);
185
185
  rooms.set(key, it);
186
186
  }
package/src/vite.js CHANGED
@@ -6,8 +6,9 @@
6
6
  // with a backend gets a second chunk beside it, `backend.js`. In dev: the
7
7
  // route that carries a model call to its provider with the key from the
8
8
  // developer's environment, the host that runs the game's backend for the
9
- // tabs that join it, and the script that starts each page's storage empty
10
- // for the game's saves. Every other build is untouched. The plugin
9
+ // tabs that join it, and the scripts that start each page's storage empty
10
+ // for the game's saves and offer `dewdrop.evaluate`. Every other build is
11
+ // untouched. The plugin
11
12
  // imports nothing from Vite itself, so it works linked from a checkout as
12
13
  // well as installed, on Vite 3 through 8.
13
14
  import { existsSync, readFileSync } from 'node:fs';
@@ -15,6 +16,7 @@ import { resolve } from 'node:path';
15
16
  import { ASSETS_DIR, BACKEND_FILE, ENTRY_FILE, PAGE_FILE, detectViteMajor, openPage, outputOptions } from './build.js';
16
17
  import { createLlmMiddleware } from './dev.js';
17
18
  import { loadKeys } from './env.js';
19
+ import { evaluateScript } from './evaluate.js';
18
20
  import { freshScript } from './fresh.js';
19
21
  import { DEV_ROUTE } from './route.js';
20
22
  import { createBackendHost } from './serve.js';
@@ -140,14 +142,15 @@ export default function dew() {
140
142
  },
141
143
  },
142
144
  // The page the dev server serves is told whether the game has a
143
- // backend, which the client's `backend` reads before it joins, and
144
- // starts with its storage empty, as on Dewdrop, from a script first
145
- // in the page, before anything of its own.
145
+ // backend, which the client's `backend` reads before it joins, starts
146
+ // with its storage empty, as on Dewdrop, and offers `dewdrop.evaluate`,
147
+ // as a dev version's page does, from a script first in the page,
148
+ // before anything of its own.
146
149
  // A build's page, which no dev server serves, is left alone.
147
150
  transformIndexHtml(html) {
148
151
  if (!serving) return undefined;
149
152
  return {
150
- html: withFirstScript(html, freshScript(game)),
153
+ html: withFirstScript(html, freshScript(game) + evaluateScript()),
151
154
  tags: [{ tag: 'meta', attrs: { name: 'dew-backend', content: entry === null ? 'no' : 'yes' }, injectTo: 'head' }],
152
155
  };
153
156
  },