ugly-game 0.7.1 → 0.7.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/loop/controlBlock.d.ts +6 -0
- package/dist/loop/controlBlock.js +7 -0
- package/dist/loop/simCore.d.ts +6 -0
- package/dist/loop/simCore.js +8 -1
- package/dist/loop/simHost.d.ts +19 -1
- package/dist/loop/simHost.js +89 -22
- package/dist/loop/simHost.worker.d.ts +13 -0
- package/dist/loop/simHost.worker.js +41 -7
- package/package.json +1 -1
|
@@ -30,6 +30,12 @@ export interface ControlView<S extends string> {
|
|
|
30
30
|
raw(): Int32Array;
|
|
31
31
|
/** Atomically read-and-zero STEP_BUDGET — the worker consumes the burst so it can't be applied twice. */
|
|
32
32
|
takeStepBudget(): number;
|
|
33
|
+
/** Atomically add to the LATE counter — the worker flags each command that arrived after its tick (co-op
|
|
34
|
+
* divergence signal). Add (not store) so concurrent late flags in one batch accumulate. */
|
|
35
|
+
addLate(n: number): void;
|
|
36
|
+
/** Atomically read-and-zero LATE — the main thread drains the divergence count each frame without racing the
|
|
37
|
+
* worker's adds (an `exchange`, so an add landing between read and reset is never lost). */
|
|
38
|
+
takeLate(): number;
|
|
33
39
|
/** Spin-wait (bounded) until PAUSED reaches `target`; returns true if it did. Main-thread safe (no Atomics.wait). */
|
|
34
40
|
waitPaused(target: boolean, spinMax?: number): boolean;
|
|
35
41
|
notify(slot: number): void;
|
|
@@ -104,6 +104,13 @@ export function createControlView(buf, scalarNames) {
|
|
|
104
104
|
takeStepBudget() {
|
|
105
105
|
return Atomics.exchange(a, H.STEP_BUDGET, 0);
|
|
106
106
|
},
|
|
107
|
+
addLate(n) {
|
|
108
|
+
if (n)
|
|
109
|
+
Atomics.add(a, H.LATE, n | 0);
|
|
110
|
+
},
|
|
111
|
+
takeLate() {
|
|
112
|
+
return Atomics.exchange(a, H.LATE, 0);
|
|
113
|
+
},
|
|
107
114
|
scalar(name) {
|
|
108
115
|
const slot = idx.get(name);
|
|
109
116
|
if (slot === undefined) {
|
package/dist/loop/simCore.d.ts
CHANGED
|
@@ -21,6 +21,10 @@ export interface CoopStepOpts<S, I> {
|
|
|
21
21
|
tickOf: (s: S) => number;
|
|
22
22
|
maxCatchUpFrames?: number;
|
|
23
23
|
onLate?: (count: number) => void;
|
|
24
|
+
/** Called once per tick, with the commands due THIS tick, immediately before `sim.tick`. The seam a game uses
|
|
25
|
+
* to drive per-tick co-op diagnostics (a command journal + a checkpoint hash trail) without the primitive
|
|
26
|
+
* needing to know their shape. `state` is passed so a checkpoint can hash it (at the top of the tick). */
|
|
27
|
+
beforeTick?: (due: I[], state: S) => void;
|
|
24
28
|
}
|
|
25
29
|
export interface SoloStepOpts<S, I> {
|
|
26
30
|
sim: SimHostSim<S, I>;
|
|
@@ -34,6 +38,8 @@ export interface SoloStepOpts<S, I> {
|
|
|
34
38
|
};
|
|
35
39
|
fps?: number;
|
|
36
40
|
maxCatchUpFrames?: number;
|
|
41
|
+
onLate?: (count: number) => void;
|
|
42
|
+
beforeTick?: (due: I[], state: S) => void;
|
|
37
43
|
}
|
|
38
44
|
/** Single-player stepping: derive a target tick from elapsed wall time (a free-run 60-UPS accumulator, clamped by
|
|
39
45
|
* maxCatchUpFrames), publish it to `ctl.targetTick`, then reuse `coopStepBatch` — so solo and co-op run the identical
|
package/dist/loop/simCore.js
CHANGED
|
@@ -20,6 +20,8 @@ export function soloStepBatch(opts, acc) {
|
|
|
20
20
|
queue: opts.queue,
|
|
21
21
|
tickOf: opts.tickOf,
|
|
22
22
|
maxCatchUpFrames: maxCatch,
|
|
23
|
+
...(opts.onLate ? { onLate: opts.onLate } : {}),
|
|
24
|
+
...(opts.beforeTick ? { beforeTick: opts.beforeTick } : {}),
|
|
23
25
|
});
|
|
24
26
|
if (stepped > 0)
|
|
25
27
|
opts.ctl.ups = Math.round(stepped / Math.max(elapsed, T)) || opts.ctl.ups;
|
|
@@ -43,7 +45,12 @@ export function coopStepBatch(opts) {
|
|
|
43
45
|
if (late && opts.onLate)
|
|
44
46
|
opts.onLate(late);
|
|
45
47
|
}
|
|
46
|
-
|
|
48
|
+
const inputs = due.map((d) => d.c);
|
|
49
|
+
// Per-tick diagnostics seam: the game records the applied stream + a checkpoint hash of the top-of-tick state,
|
|
50
|
+
// BEFORE the tick mutates it. Fires every tick (solo + co-op) so a journal can't silently skip history.
|
|
51
|
+
if (opts.beforeTick)
|
|
52
|
+
opts.beforeTick(inputs, opts.state);
|
|
53
|
+
opts.sim.tick(opts.state, inputs);
|
|
47
54
|
stepped++;
|
|
48
55
|
};
|
|
49
56
|
// STEP_BUDGET burst first (forced, deterministic — independent of the target).
|
package/dist/loop/simHost.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { type ControlView } from './controlBlock';
|
|
|
3
3
|
import { type SimClock, type SimHostSim } from './simCore';
|
|
4
4
|
import { type Lockstep, type LockstepTransport, type LockstepClock } from './lockstep';
|
|
5
5
|
import type { NetAdapter } from './gameLoop';
|
|
6
|
+
import type { SimWorkerReply } from './simHost.worker';
|
|
6
7
|
export interface WorkerLike {
|
|
7
8
|
postMessage(m: unknown): void;
|
|
8
9
|
onmessage: ((e: {
|
|
@@ -17,6 +18,7 @@ export interface CoopOpts {
|
|
|
17
18
|
hashEvery?: number;
|
|
18
19
|
onNeedSnap?: (from: string) => void;
|
|
19
20
|
onSnapReady?: (from: string) => void;
|
|
21
|
+
onLate?: (n: number) => void;
|
|
20
22
|
}
|
|
21
23
|
export interface SimHostOpts<State, Input> {
|
|
22
24
|
layout: Layout<State>;
|
|
@@ -35,6 +37,8 @@ export interface SimHostOpts<State, Input> {
|
|
|
35
37
|
initParams?: Record<string, unknown>;
|
|
36
38
|
}
|
|
37
39
|
export interface SimHost<State, Input> {
|
|
40
|
+
/** The live sim state view. In the worker path it's a zero-copy SAB view the worker mutates; a world-swap
|
|
41
|
+
* (`reinit`) re-points it, so read it fresh each frame rather than capturing it once. */
|
|
38
42
|
readonly state: State;
|
|
39
43
|
readonly available: boolean;
|
|
40
44
|
send(input: Input): void;
|
|
@@ -43,8 +47,22 @@ export interface SimHost<State, Input> {
|
|
|
43
47
|
hash(): string;
|
|
44
48
|
netAdapter(): NetAdapter;
|
|
45
49
|
lockstep?: Lockstep<Input>;
|
|
50
|
+
/** Call once per rendered frame. Co-op: publishes the server-derived `targetTick`, emits the probe hash on the
|
|
51
|
+
* grid, and drains the LATE divergence count (→ `coop.onLate`). In the fallback (no-worker) path it also steps
|
|
52
|
+
* the sim to `nowMs`. A worker-backed solo host needs no per-frame call, but calling `frame` is harmless. */
|
|
53
|
+
frame(nowMs: number): void;
|
|
54
|
+
/** Swap the whole world (new game / load / resync) to `fill` over a fresh buffer of `layout`'s size. `resync`
|
|
55
|
+
* keeps in-flight future commands (drops applied history); otherwise the command stream restarts. */
|
|
56
|
+
reinit(layout: Layout<State>, fill: (s: State) => void, opts?: {
|
|
57
|
+
resync?: boolean;
|
|
58
|
+
initParams?: Record<string, unknown>;
|
|
59
|
+
}): void;
|
|
60
|
+
/** Round-trip a message to the worker and await its reply (echoing `rid`). Resolves `null` in the fallback path
|
|
61
|
+
* (no worker) or on timeout — the caller computes a degraded answer locally. */
|
|
62
|
+
request(msg: Record<string, unknown>): Promise<SimWorkerReply | null>;
|
|
46
63
|
stop(): void;
|
|
47
|
-
/** Fallback/test only: advance the main-thread sim to wall time `nowMs`. Undefined when a worker owns stepping.
|
|
64
|
+
/** Fallback/test only: advance the main-thread sim to wall time `nowMs`. Undefined when a worker owns stepping.
|
|
65
|
+
* Retained for existing callers/tests; `frame` supersedes it. */
|
|
48
66
|
pump?(nowMs: number): void;
|
|
49
67
|
}
|
|
50
68
|
export declare function createSimHost<State, Input>(opts: SimHostOpts<State, Input>): SimHost<State, Input>;
|
package/dist/loop/simHost.js
CHANGED
|
@@ -15,7 +15,7 @@ export function createSimHost(opts) {
|
|
|
15
15
|
const ctlBuf = useWorker
|
|
16
16
|
? new SharedArrayBuffer(controlByteLength(scalars))
|
|
17
17
|
: new ArrayBuffer(controlByteLength(scalars));
|
|
18
|
-
|
|
18
|
+
let state = opts.layout.attach(dataBuf); // `let`: a world-swap (reinit) re-points this at a fresh buffer's view
|
|
19
19
|
const ctl = createControlView(ctlBuf, scalars);
|
|
20
20
|
opts.seed?.(state);
|
|
21
21
|
const clock = opts.clock ?? { now: () => Date.now() };
|
|
@@ -24,16 +24,28 @@ export function createSimHost(opts) {
|
|
|
24
24
|
const sampled = new Map(); // our probe hash per grid tick (for divergence compare)
|
|
25
25
|
let lastHash = opts.sim.hash(state);
|
|
26
26
|
// ── Worker path ──────────────────────────────────────────────
|
|
27
|
+
// request()/reply round-trip: the game's worker `onMessage` answers a diagnostics request by posting a reply that
|
|
28
|
+
// echoes `rid`; we resolve the matching pending Promise here. Keyed on `rid` alone, so it's oblivious to payload.
|
|
29
|
+
let reqSeq = 0;
|
|
30
|
+
const reqWait = new Map();
|
|
27
31
|
let worker = null;
|
|
28
32
|
if (useWorker && opts.worker) {
|
|
29
33
|
worker = opts.worker();
|
|
30
34
|
worker.onmessage = (e) => {
|
|
31
35
|
const m = e.data;
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
+
if (typeof m.rid === 'number') {
|
|
37
|
+
const r = reqWait.get(m.rid);
|
|
38
|
+
if (r) {
|
|
39
|
+
reqWait.delete(m.rid);
|
|
40
|
+
r(m);
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
36
43
|
}
|
|
44
|
+
// The worker's posted hash is the FULL digest → keep host.hash() cheap. Deliberately NOT written into
|
|
45
|
+
// `sampled`: co-op compares the PROBE hash (sampled on the main thread in `drive`), and mixing the two oracles
|
|
46
|
+
// would report perpetual divergence.
|
|
47
|
+
if (m.t === 'hash' && typeof m.hash === 'string')
|
|
48
|
+
lastHash = m.hash;
|
|
37
49
|
};
|
|
38
50
|
worker.postMessage({
|
|
39
51
|
t: 'init',
|
|
@@ -70,28 +82,36 @@ export function createSimHost(opts) {
|
|
|
70
82
|
});
|
|
71
83
|
ctl.coop = true;
|
|
72
84
|
}
|
|
73
|
-
// ──
|
|
85
|
+
// ── Per-frame driver ─────────────────────────────────────────
|
|
86
|
+
// Co-op bookkeeping runs on the MAIN thread from the shared state, so the local sample and the value sent to
|
|
87
|
+
// peers are the SAME oracle (`probe`): the worker's own posted hash is the FULL digest, used only for host.hash()
|
|
88
|
+
// — mixing it into `sampled` would compare full-vs-probe and cry divergence forever. In the fallback path this
|
|
89
|
+
// same driver also steps the sim (there's no worker to do it).
|
|
74
90
|
const lastRef = { t: clock.now() };
|
|
75
91
|
let acc = 0;
|
|
76
92
|
const hashEvery = opts.coop?.hashEvery ?? 600;
|
|
77
|
-
const
|
|
78
|
-
const drivenClock = { now: () => nowMs };
|
|
93
|
+
const drive = (nowMs, doStep) => {
|
|
79
94
|
if (lockstep) {
|
|
80
|
-
const tk = opts.tickOf(state);
|
|
81
|
-
if (tk % hashEvery === 0)
|
|
95
|
+
const tk = worker ? ctl.tick : opts.tickOf(state);
|
|
96
|
+
if (tk % hashEvery === 0 && !sampled.has(tk))
|
|
82
97
|
sampled.set(tk, probe());
|
|
83
98
|
ctl.targetTick = lockstep.targetTick();
|
|
84
99
|
lockstep.onFrame(tk);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
100
|
+
if (doStep)
|
|
101
|
+
coopStepBatch({
|
|
102
|
+
sim: opts.sim,
|
|
103
|
+
state,
|
|
104
|
+
ctl,
|
|
105
|
+
queue,
|
|
106
|
+
tickOf: opts.tickOf,
|
|
107
|
+
maxCatchUpFrames: opts.maxCatchUpFrames ?? 30,
|
|
108
|
+
});
|
|
109
|
+
const late = ctl.takeLate();
|
|
110
|
+
if (late > 0)
|
|
111
|
+
opts.coop?.onLate?.(late);
|
|
93
112
|
}
|
|
94
|
-
else {
|
|
113
|
+
else if (doStep) {
|
|
114
|
+
const drivenClock = { now: () => nowMs };
|
|
95
115
|
acc = soloStepBatch({
|
|
96
116
|
sim: opts.sim,
|
|
97
117
|
state,
|
|
@@ -104,7 +124,11 @@ export function createSimHost(opts) {
|
|
|
104
124
|
maxCatchUpFrames: opts.maxCatchUpFrames,
|
|
105
125
|
}, acc);
|
|
106
126
|
}
|
|
107
|
-
|
|
127
|
+
if (!worker)
|
|
128
|
+
lastHash = opts.sim.hash(state); // worker keeps lastHash fresh via its posted 'hash' messages
|
|
129
|
+
};
|
|
130
|
+
const frame = (nowMs) => {
|
|
131
|
+
drive(nowMs, !worker); // worker steps itself; fallback steps in-frame
|
|
108
132
|
};
|
|
109
133
|
const noop = () => undefined;
|
|
110
134
|
const net = opts.net ?? {
|
|
@@ -113,7 +137,9 @@ export function createSimHost(opts) {
|
|
|
113
137
|
resync: () => lockstep?.needSnap(),
|
|
114
138
|
};
|
|
115
139
|
return {
|
|
116
|
-
state
|
|
140
|
+
get state() {
|
|
141
|
+
return state;
|
|
142
|
+
},
|
|
117
143
|
available: useWorker,
|
|
118
144
|
send(input) {
|
|
119
145
|
if (lockstep)
|
|
@@ -142,10 +168,51 @@ export function createSimHost(opts) {
|
|
|
142
168
|
hash: () => lastHash,
|
|
143
169
|
netAdapter: () => net,
|
|
144
170
|
lockstep,
|
|
171
|
+
frame,
|
|
172
|
+
reinit(newLayout, fill, ro) {
|
|
173
|
+
const nBuf = useWorker
|
|
174
|
+
? new SharedArrayBuffer(newLayout.byteLength)
|
|
175
|
+
: new ArrayBuffer(newLayout.byteLength);
|
|
176
|
+
state = newLayout.attach(nBuf); // fill on the main thread, then hand the ready buffer to the worker
|
|
177
|
+
fill(state);
|
|
178
|
+
lastHash = opts.sim.hash(state);
|
|
179
|
+
sampled.clear();
|
|
180
|
+
acc = 0;
|
|
181
|
+
lastRef.t = clock.now();
|
|
182
|
+
if (worker) {
|
|
183
|
+
worker.postMessage({
|
|
184
|
+
t: 'reinit',
|
|
185
|
+
dataBuf: nBuf,
|
|
186
|
+
coop: !!opts.coop,
|
|
187
|
+
resync: !!ro?.resync,
|
|
188
|
+
...(ro?.initParams ?? {}),
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
else if (ro?.resync) {
|
|
192
|
+
queue.prune(opts.tickOf(state)); // keep in-flight future commands past the snapshot tick
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
queue.clear();
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
request(msg) {
|
|
199
|
+
const w = worker;
|
|
200
|
+
if (!w)
|
|
201
|
+
return Promise.resolve(null);
|
|
202
|
+
const rid = ++reqSeq;
|
|
203
|
+
return new Promise((res) => {
|
|
204
|
+
reqWait.set(rid, res);
|
|
205
|
+
w.postMessage({ ...msg, rid });
|
|
206
|
+
setTimeout(() => {
|
|
207
|
+
if (reqWait.delete(rid))
|
|
208
|
+
res(null); // never hang a caller on a stuck worker
|
|
209
|
+
}, 4000);
|
|
210
|
+
});
|
|
211
|
+
},
|
|
145
212
|
stop() {
|
|
146
213
|
ctl.running = false;
|
|
147
214
|
worker?.terminate();
|
|
148
215
|
},
|
|
149
|
-
pump: useWorker ? undefined :
|
|
216
|
+
pump: useWorker ? undefined : frame,
|
|
150
217
|
};
|
|
151
218
|
}
|
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import type { Layout } from './layout';
|
|
2
2
|
import { type ControlView } from './controlBlock';
|
|
3
3
|
import { type SimHostSim } from './simCore';
|
|
4
|
+
/** A reply a worker posts back to the main thread. Must be structured-cloneable. Echo the originating request's
|
|
5
|
+
* `rid` so the main thread's `host.request()` Promise resolves on it; any other fields are the game's payload. */
|
|
6
|
+
export interface SimWorkerReply {
|
|
7
|
+
rid?: number;
|
|
8
|
+
[k: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
export interface SimWorkerContext<S> {
|
|
11
|
+
state: S;
|
|
12
|
+
ctl: ControlView<string>;
|
|
13
|
+
post(m: SimWorkerReply): void;
|
|
14
|
+
}
|
|
4
15
|
export interface SimWorkerConfig<S, I> {
|
|
5
16
|
sim: SimHostSim<S, I>;
|
|
6
17
|
layout: Layout<S> | ((init: Record<string, unknown>) => Layout<S>);
|
|
7
18
|
tickOf: (s: S) => number;
|
|
8
19
|
mirror?: (s: S, ctl: ControlView<string>) => void;
|
|
20
|
+
beforeTick?: (due: I[], state: S) => void;
|
|
21
|
+
onMessage?: (m: Record<string, unknown>, ctx: SimWorkerContext<S>) => void;
|
|
9
22
|
}
|
|
10
23
|
export declare function runSimWorker<S, I>(cfg: SimWorkerConfig<S, I>): void;
|
|
@@ -14,6 +14,12 @@ export function runSimWorker(cfg) {
|
|
|
14
14
|
const loop = () => {
|
|
15
15
|
if (state && ctl?.running) {
|
|
16
16
|
const before = cfg.tickOf(state);
|
|
17
|
+
// Auto-wire late-arrival counting into the control block so the main thread's `takeLate()` always reflects
|
|
18
|
+
// divergence — the game needs no code for this (it's the same signal on every SimHost game).
|
|
19
|
+
const c = ctl;
|
|
20
|
+
const onLate = (n) => {
|
|
21
|
+
c.addLate(n);
|
|
22
|
+
};
|
|
17
23
|
if (coop) {
|
|
18
24
|
coopStepBatch({
|
|
19
25
|
sim: cfg.sim,
|
|
@@ -22,6 +28,8 @@ export function runSimWorker(cfg) {
|
|
|
22
28
|
queue,
|
|
23
29
|
tickOf: cfg.tickOf,
|
|
24
30
|
maxCatchUpFrames: 30,
|
|
31
|
+
onLate,
|
|
32
|
+
...(cfg.beforeTick ? { beforeTick: cfg.beforeTick } : {}),
|
|
25
33
|
});
|
|
26
34
|
}
|
|
27
35
|
else {
|
|
@@ -34,6 +42,8 @@ export function runSimWorker(cfg) {
|
|
|
34
42
|
clock,
|
|
35
43
|
lastRef,
|
|
36
44
|
fps,
|
|
45
|
+
onLate,
|
|
46
|
+
...(cfg.beforeTick ? { beforeTick: cfg.beforeTick } : {}),
|
|
37
47
|
}, acc);
|
|
38
48
|
}
|
|
39
49
|
const stepped = cfg.tickOf(state) - before;
|
|
@@ -66,21 +76,45 @@ export function runSimWorker(cfg) {
|
|
|
66
76
|
ctx.onmessage = (e) => {
|
|
67
77
|
const m = e.data;
|
|
68
78
|
if (m.t === 'init') {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
79
|
+
const init = m;
|
|
80
|
+
const layout = typeof cfg.layout === 'function' ? cfg.layout(init) : cfg.layout;
|
|
81
|
+
state = layout.attach(init.dataBuf);
|
|
82
|
+
ctl = createControlView(init.ctlBuf, init.scalars ?? []);
|
|
83
|
+
fps = init.simFps ?? 60;
|
|
84
|
+
coop = !!init.coop;
|
|
74
85
|
ctl.running = true;
|
|
75
86
|
lastRef.t = clock.now();
|
|
76
87
|
loop();
|
|
77
88
|
}
|
|
89
|
+
else if (m.t === 'reinit') {
|
|
90
|
+
// World-swap (new game / load / resync): re-attach to the fresh, already-filled buffer (ctl + coop flag
|
|
91
|
+
// persist). A resync keeps in-flight future commands (drops applied history); otherwise the stream restarts.
|
|
92
|
+
const init = m;
|
|
93
|
+
const layout = typeof cfg.layout === 'function' ? cfg.layout(init) : cfg.layout;
|
|
94
|
+
state = layout.attach(init.dataBuf);
|
|
95
|
+
if (init.resync)
|
|
96
|
+
queue.prune(cfg.tickOf(state));
|
|
97
|
+
else
|
|
98
|
+
queue.clear();
|
|
99
|
+
lastRef.t = clock.now();
|
|
100
|
+
acc = 0;
|
|
101
|
+
}
|
|
78
102
|
else if (m.t === 'cmd') {
|
|
79
103
|
queue.push(m.s);
|
|
80
104
|
}
|
|
81
|
-
else {
|
|
82
|
-
//
|
|
105
|
+
else if (m.t === 'resync-req') {
|
|
106
|
+
// keep future commands, drop the applied history (an authoritative snapshot is being loaded).
|
|
83
107
|
queue.prune(state ? cfg.tickOf(state) : 0);
|
|
84
108
|
}
|
|
109
|
+
else if (state && ctl) {
|
|
110
|
+
// A message this runner doesn't own — hand it to the game (diagnostics requests, etc.).
|
|
111
|
+
cfg.onMessage?.(m, {
|
|
112
|
+
state,
|
|
113
|
+
ctl,
|
|
114
|
+
post: (msg) => {
|
|
115
|
+
ctx.postMessage(msg);
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
85
119
|
};
|
|
86
120
|
}
|