@linxin666/dsh-pet 0.3.13 → 0.3.15
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/lib/client.js +47 -26
- package/lib/client.js.map +1 -1
- package/lib/index.js +72 -26
- package/lib/types/client/PetSprite.d.ts.map +1 -1
- package/lib/types/client/PetSprite.js +18 -7
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +1 -0
- package/lib/types/client/pet-store.d.ts.map +1 -1
- package/lib/types/client/pet-store.js +18 -0
- package/lib/types/client/sequences.d.ts +10 -0
- package/lib/types/client/sequences.d.ts.map +1 -1
- package/lib/types/client/sequences.js +24 -16
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +27 -9
- package/lib/types/routes.d.ts +2 -1
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +58 -10
- package/package.json +14 -14
- package/src/client/PetSprite.tsx +20 -10
- package/src/client/index.test.tsx +31 -3
- package/src/client/index.ts +1 -0
- package/src/client/pet-store.test.ts +87 -0
- package/src/client/pet-store.ts +18 -0
- package/src/client/sequences.test.ts +21 -1
- package/src/client/sequences.ts +37 -16
- package/src/index.ts +27 -11
- package/src/routes.ts +60 -11
package/lib/index.js
CHANGED
|
@@ -4928,13 +4928,28 @@ function extensionOf(file) {
|
|
|
4928
4928
|
return dot < 0 ? "" : file.slice(dot).toLowerCase();
|
|
4929
4929
|
}
|
|
4930
4930
|
/**
|
|
4931
|
+
* Realpaths of containment bases, resolved once per base instead of once per
|
|
4932
|
+
* request: every asset/runtime/decoration request used to pay a base
|
|
4933
|
+
* realpathSync on the hot path. Bases are registry entry directories and the
|
|
4934
|
+
* runtime roots — an immutable, registry-bounded set. Only successes are
|
|
4935
|
+
* cached (a missing base keeps failing per request), and a symlinked base
|
|
4936
|
+
* re-pointed mid-process fails containment until restart, which is the
|
|
4937
|
+
* deny-safe direction.
|
|
4938
|
+
*/
|
|
4939
|
+
const REAL_BASE_CACHE = /* @__PURE__ */ new Map();
|
|
4940
|
+
/**
|
|
4931
4941
|
* realpath containment: resolve both sides and require the candidate to stay
|
|
4932
4942
|
* inside the base directory. A pet directory (or an atlas/preview inside it)
|
|
4933
|
-
* that is a symlink escaping its root is rejected, never followed.
|
|
4943
|
+
* that is a symlink escaping its root is rejected, never followed. The
|
|
4944
|
+
* candidate is realpath'ed live on every call; only the base side is cached.
|
|
4934
4945
|
*/
|
|
4935
4946
|
function containedRealpath(base, candidate) {
|
|
4936
4947
|
try {
|
|
4937
|
-
|
|
4948
|
+
let realBase = REAL_BASE_CACHE.get(base);
|
|
4949
|
+
if (realBase === void 0) {
|
|
4950
|
+
realBase = realpathSync(base);
|
|
4951
|
+
REAL_BASE_CACHE.set(base, realBase);
|
|
4952
|
+
}
|
|
4938
4953
|
const realCandidate = realpathSync(candidate);
|
|
4939
4954
|
return realCandidate === realBase || realCandidate.startsWith(realBase + sep) ? realCandidate : void 0;
|
|
4940
4955
|
} catch {
|
|
@@ -4955,6 +4970,20 @@ function mimeFor(file) {
|
|
|
4955
4970
|
if (dot < 0) return "application/octet-stream";
|
|
4956
4971
|
return MIME_BY_EXT[file.slice(dot).toLowerCase()] ?? "application/octet-stream";
|
|
4957
4972
|
}
|
|
4973
|
+
/** Weak validator from size + mtime, shared by the three file routes. */
|
|
4974
|
+
function weakEtag(stat) {
|
|
4975
|
+
return "\"" + stat.size.toString(16) + "-" + Math.round(stat.mtimeMs).toString(16) + "\"";
|
|
4976
|
+
}
|
|
4977
|
+
/** Answer 304 when If-None-Match matches the etag; true when handled. */
|
|
4978
|
+
function revalidated(req, res, etag) {
|
|
4979
|
+
if (req.headers["if-none-match"] !== etag) return false;
|
|
4980
|
+
res.writeHead(304, {
|
|
4981
|
+
etag,
|
|
4982
|
+
"cache-control": "no-cache"
|
|
4983
|
+
});
|
|
4984
|
+
res.end();
|
|
4985
|
+
return true;
|
|
4986
|
+
}
|
|
4958
4987
|
/** Require the method or answer 405. */
|
|
4959
4988
|
function requireMethod(req, res, method) {
|
|
4960
4989
|
if (req.method === method) return true;
|
|
@@ -5036,6 +5065,7 @@ function dirAliases(registry) {
|
|
|
5036
5065
|
*/
|
|
5037
5066
|
function assetHandler(ctx, registry, caps) {
|
|
5038
5067
|
const aliases = dirAliases(registry);
|
|
5068
|
+
const servableById = new Map(registry.entries.map((entry) => [entry.id, new Set(entry.servable)]));
|
|
5039
5069
|
return ((req, res) => {
|
|
5040
5070
|
if (!guard(ctx, req, res)) return;
|
|
5041
5071
|
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
@@ -5090,7 +5120,7 @@ function assetHandler(ctx, registry, caps) {
|
|
|
5090
5120
|
const manifestFile = join(entry.dir, MANIFEST_FILE);
|
|
5091
5121
|
file = existsSync(manifestFile) ? manifestFile : void 0;
|
|
5092
5122
|
if (file === void 0) synthesized = true;
|
|
5093
|
-
} else if (rest.length > 0 && entry.
|
|
5123
|
+
} else if (rest.length > 0 && servableById.get(entry.id)?.has(rel)) file = join(entry.dir, rel);
|
|
5094
5124
|
else if (rest.length === 2 && rest[0] === PREVIEW_DIR && PREVIEW_PATTERN.test(rest[1])) {
|
|
5095
5125
|
const preview = join(entry.dir, PREVIEW_DIR, rest[1]);
|
|
5096
5126
|
file = existsSync(preview) ? preview : void 0;
|
|
@@ -5121,8 +5151,10 @@ function assetHandler(ctx, registry, caps) {
|
|
|
5121
5151
|
return;
|
|
5122
5152
|
}
|
|
5123
5153
|
const cap = rest.length === 1 && rest[0] === MANIFEST_FILE ? caps.manifest : IMAGE_EXTENSIONS.has(extensionOf(rel)) ? caps.image : caps.model;
|
|
5154
|
+
let stat;
|
|
5124
5155
|
try {
|
|
5125
|
-
|
|
5156
|
+
stat = statSync(resolved);
|
|
5157
|
+
if (stat.size > cap) {
|
|
5126
5158
|
res.writeHead(413);
|
|
5127
5159
|
res.end();
|
|
5128
5160
|
return;
|
|
@@ -5132,11 +5164,14 @@ function assetHandler(ctx, registry, caps) {
|
|
|
5132
5164
|
res.end();
|
|
5133
5165
|
return;
|
|
5134
5166
|
}
|
|
5167
|
+
const etag = weakEtag(stat);
|
|
5168
|
+
if (revalidated(req, res, etag)) return;
|
|
5135
5169
|
return readFile(resolved).then((body) => {
|
|
5136
5170
|
res.writeHead(200, {
|
|
5137
5171
|
"content-type": mimeFor(resolved),
|
|
5138
5172
|
"content-length": String(body.byteLength),
|
|
5139
|
-
"cache-control": "no-cache"
|
|
5173
|
+
"cache-control": "no-cache",
|
|
5174
|
+
etag
|
|
5140
5175
|
});
|
|
5141
5176
|
if (req.method === "HEAD") {
|
|
5142
5177
|
res.end();
|
|
@@ -5216,8 +5251,10 @@ function runtimeHandler(ctx, roots) {
|
|
|
5216
5251
|
res.end();
|
|
5217
5252
|
return;
|
|
5218
5253
|
}
|
|
5254
|
+
let stat;
|
|
5219
5255
|
try {
|
|
5220
|
-
|
|
5256
|
+
stat = statSync(resolved);
|
|
5257
|
+
if (stat.size > 16777216) {
|
|
5221
5258
|
res.writeHead(413);
|
|
5222
5259
|
res.end();
|
|
5223
5260
|
return;
|
|
@@ -5227,11 +5264,14 @@ function runtimeHandler(ctx, roots) {
|
|
|
5227
5264
|
res.end();
|
|
5228
5265
|
return;
|
|
5229
5266
|
}
|
|
5267
|
+
const etag = weakEtag(stat);
|
|
5268
|
+
if (revalidated(req, res, etag)) return;
|
|
5230
5269
|
return readFile(resolved).then((body) => {
|
|
5231
5270
|
res.writeHead(200, {
|
|
5232
5271
|
"content-type": name.endsWith(".map") ? "application/json" : "application/javascript; charset=utf-8",
|
|
5233
5272
|
"content-length": String(body.byteLength),
|
|
5234
|
-
"cache-control": "no-cache"
|
|
5273
|
+
"cache-control": "no-cache",
|
|
5274
|
+
etag
|
|
5235
5275
|
});
|
|
5236
5276
|
if (req.method === "HEAD") {
|
|
5237
5277
|
res.end();
|
|
@@ -5332,15 +5372,8 @@ function decorationHandler(ctx, registry, caps) {
|
|
|
5332
5372
|
res.end();
|
|
5333
5373
|
return;
|
|
5334
5374
|
}
|
|
5335
|
-
const etag =
|
|
5336
|
-
if (req
|
|
5337
|
-
res.writeHead(304, {
|
|
5338
|
-
etag,
|
|
5339
|
-
"cache-control": "no-cache"
|
|
5340
|
-
});
|
|
5341
|
-
res.end();
|
|
5342
|
-
return;
|
|
5343
|
-
}
|
|
5375
|
+
const etag = weakEtag(stat);
|
|
5376
|
+
if (revalidated(req, res, etag)) return;
|
|
5344
5377
|
readFile(resolved).then((body) => {
|
|
5345
5378
|
res.writeHead(200, {
|
|
5346
5379
|
"content-type": mimeFor(resolved),
|
|
@@ -5543,17 +5576,30 @@ function applyImpl(ctx, config = {}) {
|
|
|
5543
5576
|
}
|
|
5544
5577
|
};
|
|
5545
5578
|
ctx.inject(["settings"], (settingsCtx) => {
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5579
|
+
try {
|
|
5580
|
+
const schema = makePetSettingsSchema(service.selectedPetId());
|
|
5581
|
+
if (typeof settingsCtx.settings?.installSection === "function") settingsCtx.settings.installSection(ctx, "pet", schema, base, {
|
|
5582
|
+
setSource: (source) => {
|
|
5583
|
+
current = source;
|
|
5584
|
+
},
|
|
5585
|
+
onChange: () => {
|
|
5586
|
+
const section = current();
|
|
5587
|
+
service.applySettingsSection(section);
|
|
5588
|
+
service.setEnabled(section.enabled ?? true);
|
|
5589
|
+
syncRoutes();
|
|
5590
|
+
}
|
|
5591
|
+
});
|
|
5592
|
+
else if (typeof settingsCtx.settings?.register === "function") {
|
|
5593
|
+
const scope = settingsCtx.settings.register("pet", schema, { base });
|
|
5594
|
+
current = () => scope?.get?.() ?? base;
|
|
5595
|
+
scope?.watch?.(() => {
|
|
5596
|
+
const section = current();
|
|
5597
|
+
service.applySettingsSection(section);
|
|
5598
|
+
service.setEnabled(section.enabled ?? true);
|
|
5599
|
+
syncRoutes();
|
|
5600
|
+
});
|
|
5555
5601
|
}
|
|
5556
|
-
}
|
|
5602
|
+
} catch {}
|
|
5557
5603
|
});
|
|
5558
5604
|
syncRoutes();
|
|
5559
5605
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PetSprite.d.ts","sourceRoot":"","sources":["../../../src/client/PetSprite.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAkE,SAAS,EAAE,WAAW,EAAE,MAAM,OAAO,CAAA;AAGnH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAEjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAIjD,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAA;AAGjC,wEAAwE;AACxE,MAAM,WAAW,cAAc;IAC7B,gDAAgD;IAChD,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAA;IAC7B,8EAA8E;IAC9E,UAAU,EAAE,aAAa,CAAA;IACzB,qDAAqD;IACrD,OAAO,EAAE,gBAAgB,CAAA;IACzB,sCAAsC;IACtC,QAAQ,EAAE,WAAW,GAAG,IAAI,CAAA;IAC5B,8BAA8B;IAC9B,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,sCAAsC;IACtC,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,sCAAsC;IACtC,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,+BAA+B;IAC/B,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;IAClD,6EAA6E;IAC7E,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAA;IAC9C,uDAAuD;IACvD,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAChC,4DAA4D;IAC5D,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;IAC1C,2DAA2D;IAC3D,cAAc,EAAE,MAAM,IAAI,CAAA;IAC1B;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,CAAA;IAClB;;;;OAIG;IACH,GAAG,CAAC,EAAE,SAAS,CAAA;IACf;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;IAC9D;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,IAAI,CAAA;IAC3B,qEAAqE;IACrE,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,+CAA+C;IAC/C,CAAC,EAAE,WAAW,CAAC,OAAO,EAAE,CAAC,CAAA;CAC1B;AA8ID;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,WAAW,
|
|
1
|
+
{"version":3,"file":"PetSprite.d.ts","sourceRoot":"","sources":["../../../src/client/PetSprite.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAkE,SAAS,EAAE,WAAW,EAAE,MAAM,OAAO,CAAA;AAGnH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAEjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAIjD,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAA;AAGjC,wEAAwE;AACxE,MAAM,WAAW,cAAc;IAC7B,gDAAgD;IAChD,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAA;IAC7B,8EAA8E;IAC9E,UAAU,EAAE,aAAa,CAAA;IACzB,qDAAqD;IACrD,OAAO,EAAE,gBAAgB,CAAA;IACzB,sCAAsC;IACtC,QAAQ,EAAE,WAAW,GAAG,IAAI,CAAA;IAC5B,8BAA8B;IAC9B,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,sCAAsC;IACtC,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,sCAAsC;IACtC,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,+BAA+B;IAC/B,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;IAClD,6EAA6E;IAC7E,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAA;IAC9C,uDAAuD;IACvD,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAChC,4DAA4D;IAC5D,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;IAC1C,2DAA2D;IAC3D,cAAc,EAAE,MAAM,IAAI,CAAA;IAC1B;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,CAAA;IAClB;;;;OAIG;IACH,GAAG,CAAC,EAAE,SAAS,CAAA;IACf;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;IAC9D;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,IAAI,CAAA;IAC3B,qEAAqE;IACrE,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,+CAA+C;IAC/C,CAAC,EAAE,WAAW,CAAC,OAAO,EAAE,CAAC,CAAA;CAC1B;AA8ID;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,WAAW,CA+jB5D"}
|
|
@@ -14,7 +14,7 @@ import { createPortal } from 'react-dom';
|
|
|
14
14
|
import clsx from 'clsx';
|
|
15
15
|
import { announcementFresh } from "../announce.js";
|
|
16
16
|
import { framePosition, rowOfTrack, trimTrack } from "./spritesheet.js";
|
|
17
|
-
import {
|
|
17
|
+
import { createSequenceTimeline } from "./sequences.js";
|
|
18
18
|
import { animationForPhase } from "../state.js";
|
|
19
19
|
import styles from './pet.module.css';
|
|
20
20
|
/** Clamp a drag offset inside the viewport with a margin. */
|
|
@@ -230,6 +230,18 @@ export function PetSprite(props) {
|
|
|
230
230
|
const reduceMotion = typeof window !== 'undefined'
|
|
231
231
|
&& window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches === true;
|
|
232
232
|
const sequence = animation === animationForPhase(phase) ? sequences?.[phase] : undefined;
|
|
233
|
+
// Sequence state hoisted into the effect scope: the cumulative duration
|
|
234
|
+
// table and each item's trimmed track would otherwise be recomputed every
|
|
235
|
+
// tick (map/reduce plus two slices per frame), the same waste the
|
|
236
|
+
// single-track branch below avoids.
|
|
237
|
+
const timeline = sequence === undefined ? undefined : createSequenceTimeline(sequence, tracks);
|
|
238
|
+
const sequenceItems = sequence === undefined ? undefined : new Map(sequence.map(itemAnimation => {
|
|
239
|
+
const itemRow = rowOfTrack(itemAnimation);
|
|
240
|
+
return [itemAnimation, {
|
|
241
|
+
row: itemRow,
|
|
242
|
+
track: trimTrack(tracks[itemAnimation], rows[itemRow] ?? tracks[itemAnimation].frames.length),
|
|
243
|
+
}];
|
|
244
|
+
}));
|
|
233
245
|
const leadAnimation = sequence?.[0] ?? animation;
|
|
234
246
|
const row = rowOfTrack(leadAnimation);
|
|
235
247
|
const track = trimTrack(tracks[leadAnimation], rows[row] ?? tracks[leadAnimation].frames.length);
|
|
@@ -249,13 +261,12 @@ export function PetSprite(props) {
|
|
|
249
261
|
const tick = (ts) => {
|
|
250
262
|
const delta = ts - last;
|
|
251
263
|
last = ts;
|
|
252
|
-
if (
|
|
264
|
+
if (timeline !== undefined && sequenceItems !== undefined) {
|
|
253
265
|
sequenceElapsed += delta;
|
|
254
|
-
const current =
|
|
255
|
-
const
|
|
256
|
-
const
|
|
257
|
-
const
|
|
258
|
-
const pos = framePosition(cell, currentRow, col, scaleRef.current);
|
|
266
|
+
const current = timeline.frameAt(sequenceElapsed);
|
|
267
|
+
const item = sequenceItems.get(current.animation);
|
|
268
|
+
const col = item.track.frames[current.frameIndex];
|
|
269
|
+
const pos = framePosition(cell, item.row, col, scaleRef.current);
|
|
259
270
|
const posStr = pos.x + 'px ' + pos.y + 'px';
|
|
260
271
|
if (posStr !== lastPosStr) {
|
|
261
272
|
lastPosStr = posStr;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAGnE,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAA;AA8ElG,wEAAwE;AACxE,eAAO,MAAM,MAAM,UAA2E,CAAA;AAE9F,qEAAqE;AACrE,YAAY,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AACxE,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AACrD,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC7D,YAAY,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AACtF,YAAY,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAA;AACpE,YAAY,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf;;;;WAIG;QACH,aAAa,CAAC,EAAE;YAAE,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAAA;SAAE,CAAA;KAC1E;CACF;AAED;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAGnE,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAA;AA8ElG,wEAAwE;AACxE,eAAO,MAAM,MAAM,UAA2E,CAAA;AAE9F,qEAAqE;AACrE,YAAY,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AACxE,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AACrD,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC7D,YAAY,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AACtF,YAAY,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAA;AACpE,YAAY,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf;;;;WAIG;QACH,aAAa,CAAC,EAAE;YAAE,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAAA;SAAE,CAAA;KAC1E;CACF;AAED;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAqT9C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pet-store.d.ts","sourceRoot":"","sources":["../../../src/client/pet-store.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAA;AAC3F,OAAO,KAAK,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AACvE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AACpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,gDAAgD;AAChD,MAAM,WAAW,WAAW;IAC1B,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,uDAAuD;IACvD,IAAI,EAAE,cAAc,GAAG,MAAM,CAAA;IAC7B,sDAAsD;IACtD,EAAE,EAAE,MAAM,CAAA;CACX;AAED,wCAAwC;AACxC,MAAM,WAAW,UAAU;IACzB,oEAAoE;IACpE,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAA;IAC7B,0EAA0E;IAC1E,IAAI,EAAE,aAAa,EAAE,CAAA;IACrB,uBAAuB;IACvB,KAAK,EAAE,SAAS,GAAG,OAAO,GAAG,OAAO,CAAA;IACpC,iEAAiE;IACjE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,sCAAsC;IACtC,QAAQ,EAAE,WAAW,GAAG,IAAI,CAAA;CAC7B;AAED,uBAAuB;AACvB,MAAM,MAAM,YAAY,GAAG;IACzB,+CAA+C;IAC/C,WAAW,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,YAAY,KAAK,IAAI,CAAA;IAChE,iCAAiC;IACjC,OAAO,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,IAAI,CAAA;IAC3D,gCAAgC;IAChC,QAAQ,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACvF,8BAA8B;IAC9B,WAAW,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAA;IACtE;;;;OAIG;IACH,eAAe,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,oBAAoB,KAAK,IAAI,CAAA;CACzE,CAAA;AAED,0EAA0E;AAC1E,wBAAgB,cAAc,IAAI,iBAAiB,CAAC,UAAU,EAAE,YAAY,CAAC,
|
|
1
|
+
{"version":3,"file":"pet-store.d.ts","sourceRoot":"","sources":["../../../src/client/pet-store.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAA;AAC3F,OAAO,KAAK,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AACvE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AACpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,gDAAgD;AAChD,MAAM,WAAW,WAAW;IAC1B,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,uDAAuD;IACvD,IAAI,EAAE,cAAc,GAAG,MAAM,CAAA;IAC7B,sDAAsD;IACtD,EAAE,EAAE,MAAM,CAAA;CACX;AAED,wCAAwC;AACxC,MAAM,WAAW,UAAU;IACzB,oEAAoE;IACpE,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAA;IAC7B,0EAA0E;IAC1E,IAAI,EAAE,aAAa,EAAE,CAAA;IACrB,uBAAuB;IACvB,KAAK,EAAE,SAAS,GAAG,OAAO,GAAG,OAAO,CAAA;IACpC,iEAAiE;IACjE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,sCAAsC;IACtC,QAAQ,EAAE,WAAW,GAAG,IAAI,CAAA;CAC7B;AAED,uBAAuB;AACvB,MAAM,MAAM,YAAY,GAAG;IACzB,+CAA+C;IAC/C,WAAW,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,YAAY,KAAK,IAAI,CAAA;IAChE,iCAAiC;IACjC,OAAO,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,IAAI,CAAA;IAC3D,gCAAgC;IAChC,QAAQ,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACvF,8BAA8B;IAC9B,WAAW,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAA;IACtE;;;;OAIG;IACH,eAAe,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,oBAAoB,KAAK,IAAI,CAAA;CACzE,CAAA;AAED,0EAA0E;AAC1E,wBAAgB,cAAc,IAAI,iBAAiB,CAAC,UAAU,EAAE,YAAY,CAAC,CAsC5E;AAED,YAAY,EAAE,cAAc,EAAE,CAAA;AAY9B;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,mBAAmB,CAAC,UAAU,EAAE,YAAY,CAAC,CAAA"}
|
|
@@ -18,6 +18,15 @@ export function createPetStore() {
|
|
|
18
18
|
}),
|
|
19
19
|
actions: {
|
|
20
20
|
setSnapshot: (draft, snapshot) => {
|
|
21
|
+
// The 2 s poll republishes the full snapshot even while the pet is
|
|
22
|
+
// idle. Skipping an unchanged payload keeps immer's produce at zero
|
|
23
|
+
// modifications, so zustand never notifies and the whole sprite tree
|
|
24
|
+
// skips the re-render. Equality is JSON-based and skip-only: both
|
|
25
|
+
// sides come from the same host serializer, and any mismatch falls
|
|
26
|
+
// through to the normal publish — the failure mode is one extra
|
|
27
|
+
// render, never a stale pet.
|
|
28
|
+
if (draft.state === 'ready' && draft.error === null && sameSnapshot(draft.snapshot, snapshot))
|
|
29
|
+
return;
|
|
21
30
|
draft.snapshot = snapshot;
|
|
22
31
|
draft.state = 'ready';
|
|
23
32
|
draft.error = null;
|
|
@@ -39,3 +48,12 @@ export function createPetStore() {
|
|
|
39
48
|
},
|
|
40
49
|
});
|
|
41
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Content equality for consecutive poll snapshots. JSON compare, not field
|
|
53
|
+
* enumeration: an exact string match is the only way to skip the publish, so
|
|
54
|
+
* a missed field can never freeze the UI — it can only cost the render the
|
|
55
|
+
* optimization exists to save.
|
|
56
|
+
*/
|
|
57
|
+
function sameSnapshot(previous, next) {
|
|
58
|
+
return previous !== null && JSON.stringify(previous) === JSON.stringify(next);
|
|
59
|
+
}
|
|
@@ -5,6 +5,16 @@ export interface SequenceFrame {
|
|
|
5
5
|
animation: PetAnimation;
|
|
6
6
|
frameIndex: number;
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Precomputed timeline for one looping sequence: the per-item duration table
|
|
10
|
+
* is built once instead of per query. The sprite frame loop asks at
|
|
11
|
+
* animation rate (~60 Hz), where the per-call map/reduce is pure waste.
|
|
12
|
+
*/
|
|
13
|
+
export interface SequenceTimeline {
|
|
14
|
+
frameAt(elapsedMs: number): SequenceFrame;
|
|
15
|
+
}
|
|
16
|
+
/** Build a {@link SequenceTimeline} over one manifest sequence. */
|
|
17
|
+
export declare function createSequenceTimeline(sequence: readonly PetAnimation[], tracks: Record<PetAnimation, PetTrackDef>): SequenceTimeline;
|
|
8
18
|
/** Resolve the active track and frame after elapsed milliseconds of a looping sequence. */
|
|
9
19
|
export declare function sequenceFrameAt(sequence: readonly PetAnimation[], tracks: Record<PetAnimation, PetTrackDef>, elapsedMs: number): SequenceFrame;
|
|
10
20
|
//# sourceMappingURL=sequences.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sequences.d.ts","sourceRoot":"","sources":["../../../src/client/sequences.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAE1E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAE/C,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,YAAY,CAAA;IACvB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,2FAA2F;AAC3F,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,SAAS,YAAY,EAAE,EACjC,MAAM,EAAE,MAAM,CAAC,YAAY,EAAE,WAAW,CAAC,EACzC,SAAS,EAAE,MAAM,GAChB,aAAa,
|
|
1
|
+
{"version":3,"file":"sequences.d.ts","sourceRoot":"","sources":["../../../src/client/sequences.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAE1E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAE/C,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,YAAY,CAAA;IACvB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,aAAa,CAAA;CAC1C;AAED,mEAAmE;AACnE,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,SAAS,YAAY,EAAE,EACjC,MAAM,EAAE,MAAM,CAAC,YAAY,EAAE,WAAW,CAAC,GACxC,gBAAgB,CAqBlB;AAED,2FAA2F;AAC3F,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,SAAS,YAAY,EAAE,EACjC,MAAM,EAAE,MAAM,CAAC,YAAY,EAAE,WAAW,CAAC,EACzC,SAAS,EAAE,MAAM,GAChB,aAAa,CAEf"}
|
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
/** Pure timing helpers for manifest-defined scene animation sequences. */
|
|
2
|
-
/**
|
|
3
|
-
export function
|
|
2
|
+
/** Build a {@link SequenceTimeline} over one manifest sequence. */
|
|
3
|
+
export function createSequenceTimeline(sequence, tracks) {
|
|
4
4
|
const itemDurations = sequence.map(animation => tracks[animation].durations.reduce((sum, value) => sum + value, 0));
|
|
5
5
|
const sequenceDuration = itemDurations.reduce((sum, value) => sum + value, 0);
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
6
|
+
return {
|
|
7
|
+
frameAt(elapsedMs) {
|
|
8
|
+
let offset = Math.max(0, elapsedMs) % sequenceDuration;
|
|
9
|
+
let itemIndex = 0;
|
|
10
|
+
while (itemIndex < sequence.length - 1 && offset >= itemDurations[itemIndex]) {
|
|
11
|
+
offset -= itemDurations[itemIndex];
|
|
12
|
+
itemIndex += 1;
|
|
13
|
+
}
|
|
14
|
+
const animation = sequence[itemIndex];
|
|
15
|
+
const track = tracks[animation];
|
|
16
|
+
let frameIndex = 0;
|
|
17
|
+
while (frameIndex < track.frames.length - 1 && offset >= track.durations[frameIndex]) {
|
|
18
|
+
offset -= track.durations[frameIndex];
|
|
19
|
+
frameIndex += 1;
|
|
20
|
+
}
|
|
21
|
+
return { animation, frameIndex };
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** Resolve the active track and frame after elapsed milliseconds of a looping sequence. */
|
|
26
|
+
export function sequenceFrameAt(sequence, tracks, elapsedMs) {
|
|
27
|
+
return createSequenceTimeline(sequence, tracks).frameAt(elapsedMs);
|
|
20
28
|
}
|
package/lib/types/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAG7C,OAAO,CAAC,MAAM,aAAa,CAAA;AAC3B,OAAO,EAAsC,KAAK,SAAS,EAA2B,MAAM,cAAc,CAAA;AAM1G,OAAO,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAC9D,YAAY,EACV,SAAS,EACT,iBAAiB,EACjB,kBAAkB,EAClB,cAAc,EACd,YAAY,GACb,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,YAAY,EACZ,cAAc,EACd,gBAAgB,EAChB,eAAe,EACf,aAAa,EACb,MAAM,GACP,MAAM,eAAe,CAAA;AACtB,YAAY,EACV,cAAc,EACd,aAAa,EACb,kBAAkB,EAClB,cAAc,GACf,MAAM,eAAe,CAAA;AACtB,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,KAAK,GACN,MAAM,YAAY,CAAA;AACnB,YAAY,EACV,aAAa,EACb,YAAY,EACZ,cAAc,EACd,aAAa,EACb,gBAAgB,GACjB,MAAM,YAAY,CAAA;AACnB,OAAO,EACL,YAAY,EACZ,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,aAAa,CAAA;AACpB,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAC5E,OAAO,EACL,eAAe,EACf,YAAY,EACZ,eAAe,EACf,gBAAgB,EAChB,YAAY,EACZ,aAAa,EACb,mBAAmB,GACpB,MAAM,cAAc,CAAA;AACrB,YAAY,EAAE,UAAU,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAC9E,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,mBAAmB,EACnB,oBAAoB,EACpB,YAAY,EACZ,cAAc,EACd,UAAU,EACV,cAAc,GACf,MAAM,cAAc,CAAA;AACrB,YAAY,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAChE,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,mBAAmB,EACnB,qBAAqB,EACrB,sBAAsB,EACtB,aAAa,EACb,YAAY,EACZ,eAAe,EACf,YAAY,EACZ,cAAc,EACd,kBAAkB,GACnB,MAAM,eAAe,CAAA;AACtB,YAAY,EACV,aAAa,EACb,QAAQ,EACR,WAAW,EACX,WAAW,EACX,kBAAkB,EAClB,WAAW,EACX,gBAAgB,GACjB,MAAM,eAAe,CAAA;AAEtB,OAAO,EACL,aAAa,EACb,cAAc,EACd,gBAAgB,GACjB,MAAM,aAAa,CAAA;AAEpB,sEAAsE;AACtE,eAAO,MAAM,IAAI,QAAQ,CAAA;AAEzB,+DAA+D;AAC/D,eAAO,MAAM,MAAM,UAAgB,CAAA;AAEnC;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,aAAa,EAAE,MAAM;;;;;;;;;;;;;;;;IAU1D;AAED,0EAA0E;AAC1E,eAAO,MAAM,KAAK,kBAA6C,CAAA;AAE/D,iBAAS,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE,SAAc,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAG7C,OAAO,CAAC,MAAM,aAAa,CAAA;AAC3B,OAAO,EAAsC,KAAK,SAAS,EAA2B,MAAM,cAAc,CAAA;AAM1G,OAAO,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAC9D,YAAY,EACV,SAAS,EACT,iBAAiB,EACjB,kBAAkB,EAClB,cAAc,EACd,YAAY,GACb,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,YAAY,EACZ,cAAc,EACd,gBAAgB,EAChB,eAAe,EACf,aAAa,EACb,MAAM,GACP,MAAM,eAAe,CAAA;AACtB,YAAY,EACV,cAAc,EACd,aAAa,EACb,kBAAkB,EAClB,cAAc,GACf,MAAM,eAAe,CAAA;AACtB,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,KAAK,GACN,MAAM,YAAY,CAAA;AACnB,YAAY,EACV,aAAa,EACb,YAAY,EACZ,cAAc,EACd,aAAa,EACb,gBAAgB,GACjB,MAAM,YAAY,CAAA;AACnB,OAAO,EACL,YAAY,EACZ,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,aAAa,CAAA;AACpB,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAC5E,OAAO,EACL,eAAe,EACf,YAAY,EACZ,eAAe,EACf,gBAAgB,EAChB,YAAY,EACZ,aAAa,EACb,mBAAmB,GACpB,MAAM,cAAc,CAAA;AACrB,YAAY,EAAE,UAAU,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAC9E,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,mBAAmB,EACnB,oBAAoB,EACpB,YAAY,EACZ,cAAc,EACd,UAAU,EACV,cAAc,GACf,MAAM,cAAc,CAAA;AACrB,YAAY,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAChE,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,mBAAmB,EACnB,qBAAqB,EACrB,sBAAsB,EACtB,aAAa,EACb,YAAY,EACZ,eAAe,EACf,YAAY,EACZ,cAAc,EACd,kBAAkB,GACnB,MAAM,eAAe,CAAA;AACtB,YAAY,EACV,aAAa,EACb,QAAQ,EACR,WAAW,EACX,WAAW,EACX,kBAAkB,EAClB,WAAW,EACX,gBAAgB,GACjB,MAAM,eAAe,CAAA;AAEtB,OAAO,EACL,aAAa,EACb,cAAc,EACd,gBAAgB,GACjB,MAAM,aAAa,CAAA;AAEpB,sEAAsE;AACtE,eAAO,MAAM,IAAI,QAAQ,CAAA;AAEzB,+DAA+D;AAC/D,eAAO,MAAM,MAAM,UAAgB,CAAA;AAEnC;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,aAAa,EAAE,MAAM;;;;;;;;;;;;;;;;IAU1D;AAED,0EAA0E;AAC1E,eAAO,MAAM,KAAK,kBAA6C,CAAA;AAE/D,iBAAS,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE,SAAc,GAAG,IAAI,CAkF7D"}
|
package/lib/types/index.js
CHANGED
|
@@ -96,15 +96,33 @@ function applyImpl(ctx, config = {}) {
|
|
|
96
96
|
}
|
|
97
97
|
};
|
|
98
98
|
ctx.inject(['settings'], (settingsCtx) => {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
99
|
+
try {
|
|
100
|
+
const schema = makePetSettingsSchema(service.selectedPetId());
|
|
101
|
+
if (typeof settingsCtx.settings?.installSection === 'function') {
|
|
102
|
+
settingsCtx.settings.installSection(ctx, PET_SETTINGS_NAMESPACE, schema, base, {
|
|
103
|
+
setSource: (source) => { current = source; },
|
|
104
|
+
onChange: () => {
|
|
105
|
+
const section = current();
|
|
106
|
+
service.applySettingsSection(section);
|
|
107
|
+
service.setEnabled(section.enabled ?? true);
|
|
108
|
+
syncRoutes();
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
else if (typeof settingsCtx.settings?.register === 'function') {
|
|
113
|
+
const scope = settingsCtx.settings.register(PET_SETTINGS_NAMESPACE, schema, { base });
|
|
114
|
+
current = () => scope?.get?.() ?? base;
|
|
115
|
+
scope?.watch?.(() => {
|
|
116
|
+
const section = current();
|
|
117
|
+
service.applySettingsSection(section);
|
|
118
|
+
service.setEnabled(section.enabled ?? true);
|
|
119
|
+
syncRoutes();
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Defensive fallback against settings registration differences
|
|
125
|
+
}
|
|
108
126
|
});
|
|
109
127
|
syncRoutes();
|
|
110
128
|
}
|
package/lib/types/routes.d.ts
CHANGED
|
@@ -40,7 +40,8 @@ export interface PetAssetCaps {
|
|
|
40
40
|
/**
|
|
41
41
|
* realpath containment: resolve both sides and require the candidate to stay
|
|
42
42
|
* inside the base directory. A pet directory (or an atlas/preview inside it)
|
|
43
|
-
* that is a symlink escaping its root is rejected, never followed.
|
|
43
|
+
* that is a symlink escaping its root is rejected, never followed. The
|
|
44
|
+
* candidate is realpath'ed live on every call; only the base side is cached.
|
|
44
45
|
*/
|
|
45
46
|
export declare function containedRealpath(base: string, candidate: string): string | undefined;
|
|
46
47
|
/** Browser-facing base path of the plugin runtime files (pet-center M3). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAO9C,+CAA+C;AAC/C,eAAO,MAAM,cAAc,aAAa,CAAA;AAExC,0EAA0E;AAC1E,eAAO,MAAM,gBAAgB,SAAS,CAAA;AAMtC;;;;GAIG;AACH,eAAO,MAAM,cAAc;IACzB,yBAAyB;;IAEzB,iDAAiD;;IAEjD,8EAA8E;;CAEtE,CAAA;AAEV,6DAA6D;AAC7D,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAO9C,+CAA+C;AAC/C,eAAO,MAAM,cAAc,aAAa,CAAA;AAExC,0EAA0E;AAC1E,eAAO,MAAM,gBAAgB,SAAS,CAAA;AAMtC;;;;GAIG;AACH,eAAO,MAAM,cAAc;IACzB,yBAAyB;;IAEzB,iDAAiD;;IAEjD,8EAA8E;;CAEtE,CAAA;AAEV,6DAA6D;AAC7D,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAsBD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAcrF;AAqPD,4EAA4E;AAC5E,eAAO,MAAM,kBAAkB,QAA8B,CAAA;AAe7D,4EAA4E;AAC5E,eAAO,MAAM,eAAe,QAAmB,CAAA;AAE/C,6EAA6E;AAC7E,MAAM,WAAW,eAAe;IAC9B,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AA4MD,4EAA4E;AAC5E,wBAAgB,aAAa,CAAC,IAAI,EAAE;IAAE,OAAO,EAAE,UAAU,CAAC;IAAC,GAAG,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,YAAY,CAAA;CAAE,GAAG,eAAe,GAAG,QAAQ,EAAE,CAgFjI;AAGD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA"}
|
package/lib/types/routes.js
CHANGED
|
@@ -45,14 +45,29 @@ function extensionOf(file) {
|
|
|
45
45
|
const dot = file.lastIndexOf('.');
|
|
46
46
|
return dot < 0 ? '' : file.slice(dot).toLowerCase();
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Realpaths of containment bases, resolved once per base instead of once per
|
|
50
|
+
* request: every asset/runtime/decoration request used to pay a base
|
|
51
|
+
* realpathSync on the hot path. Bases are registry entry directories and the
|
|
52
|
+
* runtime roots — an immutable, registry-bounded set. Only successes are
|
|
53
|
+
* cached (a missing base keeps failing per request), and a symlinked base
|
|
54
|
+
* re-pointed mid-process fails containment until restart, which is the
|
|
55
|
+
* deny-safe direction.
|
|
56
|
+
*/
|
|
57
|
+
const REAL_BASE_CACHE = new Map();
|
|
48
58
|
/**
|
|
49
59
|
* realpath containment: resolve both sides and require the candidate to stay
|
|
50
60
|
* inside the base directory. A pet directory (or an atlas/preview inside it)
|
|
51
|
-
* that is a symlink escaping its root is rejected, never followed.
|
|
61
|
+
* that is a symlink escaping its root is rejected, never followed. The
|
|
62
|
+
* candidate is realpath'ed live on every call; only the base side is cached.
|
|
52
63
|
*/
|
|
53
64
|
export function containedRealpath(base, candidate) {
|
|
54
65
|
try {
|
|
55
|
-
|
|
66
|
+
let realBase = REAL_BASE_CACHE.get(base);
|
|
67
|
+
if (realBase === undefined) {
|
|
68
|
+
realBase = realpathSync(base);
|
|
69
|
+
REAL_BASE_CACHE.set(base, realBase);
|
|
70
|
+
}
|
|
56
71
|
const realCandidate = realpathSync(candidate);
|
|
57
72
|
return realCandidate === realBase || realCandidate.startsWith(realBase + sep)
|
|
58
73
|
? realCandidate
|
|
@@ -77,6 +92,18 @@ function mimeFor(file) {
|
|
|
77
92
|
return 'application/octet-stream';
|
|
78
93
|
return MIME_BY_EXT[file.slice(dot).toLowerCase()] ?? 'application/octet-stream';
|
|
79
94
|
}
|
|
95
|
+
/** Weak validator from size + mtime, shared by the three file routes. */
|
|
96
|
+
function weakEtag(stat) {
|
|
97
|
+
return '"' + stat.size.toString(16) + '-' + Math.round(stat.mtimeMs).toString(16) + '"';
|
|
98
|
+
}
|
|
99
|
+
/** Answer 304 when If-None-Match matches the etag; true when handled. */
|
|
100
|
+
function revalidated(req, res, etag) {
|
|
101
|
+
if (req.headers['if-none-match'] !== etag)
|
|
102
|
+
return false;
|
|
103
|
+
res.writeHead(304, { etag, 'cache-control': 'no-cache' });
|
|
104
|
+
res.end();
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
80
107
|
/** Require the method or answer 405. */
|
|
81
108
|
function requireMethod(req, res, method) {
|
|
82
109
|
if (req.method === method)
|
|
@@ -155,6 +182,11 @@ function dirAliases(registry) {
|
|
|
155
182
|
*/
|
|
156
183
|
function assetHandler(ctx, registry, caps) {
|
|
157
184
|
const aliases = dirAliases(registry);
|
|
185
|
+
// The servable match is a Set probe instead of a linear scan: a full live2d
|
|
186
|
+
// closure routinely exceeds a hundred files, each fetched through this
|
|
187
|
+
// handler per mount. The registry is an immutable snapshot, so the sets are
|
|
188
|
+
// built once per handler.
|
|
189
|
+
const servableById = new Map(registry.entries.map(entry => [entry.id, new Set(entry.servable)]));
|
|
158
190
|
return ((req, res) => {
|
|
159
191
|
if (!guard(ctx, req, res))
|
|
160
192
|
return;
|
|
@@ -215,7 +247,7 @@ function assetHandler(ctx, registry, caps) {
|
|
|
215
247
|
if (file === undefined)
|
|
216
248
|
synthesized = true;
|
|
217
249
|
}
|
|
218
|
-
else if (rest.length > 0 && entry.
|
|
250
|
+
else if (rest.length > 0 && servableById.get(entry.id)?.has(rel)) {
|
|
219
251
|
file = join(entry.dir, rel);
|
|
220
252
|
}
|
|
221
253
|
else if (rest.length === 2 && rest[0] === PREVIEW_DIR && PREVIEW_PATTERN.test(rest[1])) {
|
|
@@ -252,8 +284,10 @@ function assetHandler(ctx, registry, caps) {
|
|
|
252
284
|
const cap = rest.length === 1 && rest[0] === MANIFEST_FILE
|
|
253
285
|
? caps.manifest
|
|
254
286
|
: IMAGE_EXTENSIONS.has(extensionOf(rel)) ? caps.image : caps.model;
|
|
287
|
+
let stat;
|
|
255
288
|
try {
|
|
256
|
-
|
|
289
|
+
stat = statSync(resolved);
|
|
290
|
+
if (stat.size > cap) {
|
|
257
291
|
res.writeHead(413);
|
|
258
292
|
res.end();
|
|
259
293
|
return;
|
|
@@ -264,11 +298,19 @@ function assetHandler(ctx, registry, caps) {
|
|
|
264
298
|
res.end();
|
|
265
299
|
return;
|
|
266
300
|
}
|
|
301
|
+
// 'no-cache' forces revalidation before every reuse, and the validator
|
|
302
|
+
// lets repeat requests settle as 304 — atlases and frames are the largest
|
|
303
|
+
// payloads the plugin serves (up to the 20 MB image cap), and without a
|
|
304
|
+
// validator every remount or page load would re-download them in full.
|
|
305
|
+
const etag = weakEtag(stat);
|
|
306
|
+
if (revalidated(req, res, etag))
|
|
307
|
+
return;
|
|
267
308
|
return readFile(resolved).then((body) => {
|
|
268
309
|
res.writeHead(200, {
|
|
269
310
|
'content-type': mimeFor(resolved),
|
|
270
311
|
'content-length': String(body.byteLength),
|
|
271
312
|
'cache-control': 'no-cache',
|
|
313
|
+
etag,
|
|
272
314
|
});
|
|
273
315
|
if (req.method === 'HEAD') {
|
|
274
316
|
res.end();
|
|
@@ -350,8 +392,10 @@ function runtimeHandler(ctx, roots) {
|
|
|
350
392
|
res.end();
|
|
351
393
|
return;
|
|
352
394
|
}
|
|
395
|
+
let stat;
|
|
353
396
|
try {
|
|
354
|
-
|
|
397
|
+
stat = statSync(resolved);
|
|
398
|
+
if (stat.size > PET_RUNTIME_CAP) {
|
|
355
399
|
res.writeHead(413);
|
|
356
400
|
res.end();
|
|
357
401
|
return;
|
|
@@ -362,11 +406,18 @@ function runtimeHandler(ctx, roots) {
|
|
|
362
406
|
res.end();
|
|
363
407
|
return;
|
|
364
408
|
}
|
|
409
|
+
// Same validator as the asset routes: the Cubism Core and vendor bundle
|
|
410
|
+
// ride every page load of a live2d pet, so revalidation should settle as
|
|
411
|
+
// 304 instead of re-downloading the full bodies.
|
|
412
|
+
const etag = weakEtag(stat);
|
|
413
|
+
if (revalidated(req, res, etag))
|
|
414
|
+
return;
|
|
365
415
|
return readFile(resolved).then((body) => {
|
|
366
416
|
res.writeHead(200, {
|
|
367
417
|
'content-type': name.endsWith('.map') ? 'application/json' : 'application/javascript; charset=utf-8',
|
|
368
418
|
'content-length': String(body.byteLength),
|
|
369
419
|
'cache-control': 'no-cache',
|
|
420
|
+
etag,
|
|
370
421
|
});
|
|
371
422
|
if (req.method === 'HEAD') {
|
|
372
423
|
res.end();
|
|
@@ -478,12 +529,9 @@ function decorationHandler(ctx, registry, caps) {
|
|
|
478
529
|
// validator lets repeat requests settle as 304 — the ornament remounts
|
|
479
530
|
// on whisper and display-session flips, and without a validator each
|
|
480
531
|
// remount would re-download the full strip body.
|
|
481
|
-
const etag =
|
|
482
|
-
if (req
|
|
483
|
-
res.writeHead(304, { etag, 'cache-control': 'no-cache' });
|
|
484
|
-
res.end();
|
|
532
|
+
const etag = weakEtag(stat);
|
|
533
|
+
if (revalidated(req, res, etag))
|
|
485
534
|
return;
|
|
486
|
-
}
|
|
487
535
|
readFile(resolved).then((body) => {
|
|
488
536
|
res.writeHead(200, {
|
|
489
537
|
'content-type': mimeFor(resolved),
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-pet",
|
|
3
3
|
"description": "Multi-pet companion plugin for the dsh web GUI: a registry-driven floating pet that reacts to model activity, with per-pet naming, petting/feeding interactions and an affinity score",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.15",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"license": "Apache-2.0",
|
|
41
41
|
"dsh": {
|
|
42
42
|
"engines": {
|
|
43
|
-
"dsh": ">=0.1.2-
|
|
43
|
+
"dsh": ">=0.1.2-rc.1"
|
|
44
44
|
},
|
|
45
45
|
"bundle": {
|
|
46
46
|
"patch": "./cordis.patch.yml"
|
|
@@ -65,18 +65,18 @@
|
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
67
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
68
|
-
"@deepseek-ai/dsh-api-remotes": "^0.1.2-
|
|
69
|
-
"@deepseek-ai/dsh-api-session-controller": "^0.1.2-
|
|
70
|
-
"@deepseek-ai/dsh-api-workspace-controller": "^0.1.2-
|
|
71
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.2-
|
|
72
|
-
"@deepseek-ai/dsh-client-store": "^0.1.2-
|
|
73
|
-
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.2-
|
|
74
|
-
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.2-
|
|
75
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.1.2-
|
|
76
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.2-
|
|
77
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.2-
|
|
78
|
-
"@deepseek-ai/dsh-session": "^0.1.2-
|
|
79
|
-
"@deepseek-ai/dsh-settings": "^0.1.2-
|
|
68
|
+
"@deepseek-ai/dsh-api-remotes": "^0.1.2-rc.1",
|
|
69
|
+
"@deepseek-ai/dsh-api-session-controller": "^0.1.2-rc.1",
|
|
70
|
+
"@deepseek-ai/dsh-api-workspace-controller": "^0.1.2-rc.1",
|
|
71
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.2-rc.1",
|
|
72
|
+
"@deepseek-ai/dsh-client-store": "^0.1.2-rc.1",
|
|
73
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.2-rc.1",
|
|
74
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.2-rc.1",
|
|
75
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.2-rc.1",
|
|
76
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.2-rc.1",
|
|
77
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.2-rc.1",
|
|
78
|
+
"@deepseek-ai/dsh-session": "^0.1.2-rc.1",
|
|
79
|
+
"@deepseek-ai/dsh-settings": "^0.1.2-rc.1",
|
|
80
80
|
"@testing-library/dom": "^10.4.1",
|
|
81
81
|
"@testing-library/react": "^16.3.2",
|
|
82
82
|
"@types/node": "^22.20.0",
|