@marktrue/dsh-mobile-remote 3.2.2-rc.1-app → 3.2.2-rc.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/CHANGELOG.md +1260 -0
- package/CONTRIBUTING.md +59 -0
- package/FAQ.md +111 -0
- package/README.md +253 -6
- package/cordis.patch.yml +14 -0
- package/docs/00-/345/274/200/345/217/221/346/200/273/347/272/262.md +125 -0
- package/docs/01-PRD.md +148 -0
- package/docs/02-architecture.md +249 -0
- package/docs/03-api.md +930 -0
- package/docs/04-security.md +100 -0
- package/docs/05-test-cases.md +366 -0
- package/docs/06-install-run.md +372 -0
- package/docs/07-user-manual.md +66 -0
- package/docs/08-/346/211/251/345/261/225/345/274/200/345/217/221/346/214/207/345/215/227.md +86 -0
- package/docs/09-compatibility.md +223 -0
- package/docs/10-release.md +929 -0
- package/docs/screenshots/01-home.png +0 -0
- package/docs/screenshots/02-notifications.png +0 -0
- package/docs/screenshots/03-sessions.png +0 -0
- package/docs/screenshots/04-chat.png +0 -0
- package/docs/screenshots/05-settings.png +0 -0
- package/docs/screenshots/06-new-session.png +0 -0
- package/docs/screenshots/pgy-appstore.jpg +0 -0
- package/docs/screenshots/pgy-mobile-app.jpg +0 -0
- package/docs/screenshots/pgy-pc-client.jpg +0 -0
- package/docs/spec-app-auto-update.md +143 -0
- package/lib/account-usage.js +145 -0
- package/lib/app-alignment.js +112 -0
- package/lib/app-carrier-store.js +1972 -0
- package/lib/app-carrier.js +609 -0
- package/lib/client.js +1174 -0
- package/lib/desktop-form.js +738 -0
- package/lib/endpoint-registry.js +165 -0
- package/lib/git-read-bundled-provider.js +255 -0
- package/lib/git-read-service.js +322 -0
- package/lib/index.js +9467 -0
- package/lib/npm-update-source.js +226 -0
- package/lib/plugin-content-digest.js +80 -0
- package/lib/plugin-self-update.js +997 -0
- package/lib/plugin-update-controller.js +274 -0
- package/lib/plugin-update-log.js +164 -0
- package/lib/plugin-update-transaction.js +322 -0
- package/lib/plugin-update.js +309 -0
- package/lib/runtime-dependency-snapshot.js +345 -0
- package/lib/session-title-cache.js +123 -0
- package/lib/session-title-refresh.js +261 -0
- package/lib/tool-process.js +20 -0
- package/lib/update-fetch.js +24 -0
- package/lib/update-lock-arbiter.js +170 -0
- package/lib/update-supervisor.js +346 -0
- package/node_modules/ansi-regex/index.d.ts +37 -0
- package/node_modules/ansi-regex/index.js +10 -0
- package/node_modules/ansi-regex/license +9 -0
- package/node_modules/ansi-regex/package.json +55 -0
- package/node_modules/ansi-regex/readme.md +78 -0
- package/node_modules/ansi-styles/index.d.ts +345 -0
- package/node_modules/ansi-styles/index.js +163 -0
- package/node_modules/ansi-styles/license +9 -0
- package/node_modules/ansi-styles/package.json +56 -0
- package/node_modules/ansi-styles/readme.md +152 -0
- package/node_modules/camelcase/index.d.ts +63 -0
- package/node_modules/camelcase/index.js +76 -0
- package/node_modules/camelcase/license +9 -0
- package/node_modules/camelcase/package.json +43 -0
- package/node_modules/camelcase/readme.md +99 -0
- package/node_modules/cliui/CHANGELOG.md +76 -0
- package/node_modules/cliui/LICENSE.txt +14 -0
- package/node_modules/cliui/README.md +115 -0
- package/node_modules/cliui/index.js +354 -0
- package/node_modules/cliui/package.json +65 -0
- package/node_modules/color-convert/CHANGELOG.md +54 -0
- package/node_modules/color-convert/LICENSE +21 -0
- package/node_modules/color-convert/README.md +68 -0
- package/node_modules/color-convert/conversions.js +839 -0
- package/node_modules/color-convert/index.js +81 -0
- package/node_modules/color-convert/package.json +48 -0
- package/node_modules/color-convert/route.js +97 -0
- package/node_modules/color-name/LICENSE +8 -0
- package/node_modules/color-name/README.md +11 -0
- package/node_modules/color-name/index.js +152 -0
- package/node_modules/color-name/package.json +28 -0
- package/node_modules/decamelize/index.js +13 -0
- package/node_modules/decamelize/license +21 -0
- package/node_modules/decamelize/package.json +38 -0
- package/node_modules/decamelize/readme.md +48 -0
- package/node_modules/dijkstrajs/.travis.yml +4 -0
- package/node_modules/dijkstrajs/CONTRIBUTING.md +8 -0
- package/node_modules/dijkstrajs/LICENSE.md +19 -0
- package/node_modules/dijkstrajs/README.md +18 -0
- package/node_modules/dijkstrajs/dijkstra.js +165 -0
- package/node_modules/dijkstrajs/package.json +60 -0
- package/node_modules/dijkstrajs/test/dijkstra.test.js +96 -0
- package/node_modules/emoji-regex/LICENSE-MIT.txt +20 -0
- package/node_modules/emoji-regex/README.md +73 -0
- package/node_modules/emoji-regex/es2015/index.js +6 -0
- package/node_modules/emoji-regex/es2015/text.js +6 -0
- package/node_modules/emoji-regex/index.d.ts +23 -0
- package/node_modules/emoji-regex/index.js +6 -0
- package/node_modules/emoji-regex/package.json +50 -0
- package/node_modules/emoji-regex/text.js +6 -0
- package/node_modules/find-up/index.d.ts +137 -0
- package/node_modules/find-up/index.js +89 -0
- package/node_modules/find-up/license +9 -0
- package/node_modules/find-up/package.json +53 -0
- package/node_modules/find-up/readme.md +156 -0
- package/node_modules/get-caller-file/LICENSE.md +6 -0
- package/node_modules/get-caller-file/README.md +41 -0
- package/node_modules/get-caller-file/index.d.ts +2 -0
- package/node_modules/get-caller-file/index.js +22 -0
- package/node_modules/get-caller-file/index.js.map +1 -0
- package/node_modules/get-caller-file/package.json +42 -0
- package/node_modules/is-fullwidth-code-point/index.d.ts +17 -0
- package/node_modules/is-fullwidth-code-point/index.js +50 -0
- package/node_modules/is-fullwidth-code-point/license +9 -0
- package/node_modules/is-fullwidth-code-point/package.json +42 -0
- package/node_modules/is-fullwidth-code-point/readme.md +39 -0
- package/node_modules/locate-path/index.d.ts +83 -0
- package/node_modules/locate-path/index.js +65 -0
- package/node_modules/locate-path/license +9 -0
- package/node_modules/locate-path/package.json +45 -0
- package/node_modules/locate-path/readme.md +122 -0
- package/node_modules/p-limit/index.d.ts +38 -0
- package/node_modules/p-limit/index.js +57 -0
- package/node_modules/p-limit/license +9 -0
- package/node_modules/p-limit/package.json +52 -0
- package/node_modules/p-limit/readme.md +101 -0
- package/node_modules/p-locate/index.d.ts +64 -0
- package/node_modules/p-locate/index.js +52 -0
- package/node_modules/p-locate/license +9 -0
- package/node_modules/p-locate/package.json +53 -0
- package/node_modules/p-locate/readme.md +90 -0
- package/node_modules/p-try/index.d.ts +39 -0
- package/node_modules/p-try/index.js +9 -0
- package/node_modules/p-try/license +9 -0
- package/node_modules/p-try/package.json +42 -0
- package/node_modules/p-try/readme.md +58 -0
- package/node_modules/path-exists/index.d.ts +28 -0
- package/node_modules/path-exists/index.js +23 -0
- package/node_modules/path-exists/license +9 -0
- package/node_modules/path-exists/package.json +39 -0
- package/node_modules/path-exists/readme.md +52 -0
- package/node_modules/pngjs/.eslintignore +2 -0
- package/node_modules/pngjs/.eslintrc.json +19 -0
- package/node_modules/pngjs/.prettierignore +3 -0
- package/node_modules/pngjs/LICENSE +20 -0
- package/node_modules/pngjs/README.md +429 -0
- package/node_modules/pngjs/browser.js +16096 -0
- package/node_modules/pngjs/coverage/lcov-report/base.css +354 -0
- package/node_modules/pngjs/coverage/lcov-report/bitmapper.js.html +881 -0
- package/node_modules/pngjs/coverage/lcov-report/bitpacker.js.html +554 -0
- package/node_modules/pngjs/coverage/lcov-report/block-navigation.js +77 -0
- package/node_modules/pngjs/coverage/lcov-report/chunkstream.js.html +647 -0
- package/node_modules/pngjs/coverage/lcov-report/constants.js.html +176 -0
- package/node_modules/pngjs/coverage/lcov-report/crc.js.html +200 -0
- package/node_modules/pngjs/coverage/lcov-report/favicon.png +0 -0
- package/node_modules/pngjs/coverage/lcov-report/filter-pack.js.html +593 -0
- package/node_modules/pngjs/coverage/lcov-report/filter-parse-async.js.html +152 -0
- package/node_modules/pngjs/coverage/lcov-report/filter-parse-sync.js.html +143 -0
- package/node_modules/pngjs/coverage/lcov-report/filter-parse.js.html +611 -0
- package/node_modules/pngjs/coverage/lcov-report/format-normaliser.js.html +359 -0
- package/node_modules/pngjs/coverage/lcov-report/index.html +426 -0
- package/node_modules/pngjs/coverage/lcov-report/interlace.js.html +365 -0
- package/node_modules/pngjs/coverage/lcov-report/packer-async.js.html +230 -0
- package/node_modules/pngjs/coverage/lcov-report/packer-sync.js.html +248 -0
- package/node_modules/pngjs/coverage/lcov-report/packer.js.html +467 -0
- package/node_modules/pngjs/coverage/lcov-report/paeth-predictor.js.html +128 -0
- package/node_modules/pngjs/coverage/lcov-report/parser-async.js.html +575 -0
- package/node_modules/pngjs/coverage/lcov-report/parser-sync.js.html +404 -0
- package/node_modules/pngjs/coverage/lcov-report/parser.js.html +950 -0
- package/node_modules/pngjs/coverage/lcov-report/png-sync.js.html +116 -0
- package/node_modules/pngjs/coverage/lcov-report/png.js.html +662 -0
- package/node_modules/pngjs/coverage/lcov-report/prettify.css +101 -0
- package/node_modules/pngjs/coverage/lcov-report/prettify.js +1007 -0
- package/node_modules/pngjs/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/node_modules/pngjs/coverage/lcov-report/sorter.js +169 -0
- package/node_modules/pngjs/coverage/lcov-report/sync-inflate.js.html +584 -0
- package/node_modules/pngjs/coverage/lcov-report/sync-reader.js.html +215 -0
- package/node_modules/pngjs/coverage/lcov.info +2177 -0
- package/node_modules/pngjs/lib/bitmapper.js +267 -0
- package/node_modules/pngjs/lib/bitpacker.js +158 -0
- package/node_modules/pngjs/lib/chunkstream.js +189 -0
- package/node_modules/pngjs/lib/constants.js +32 -0
- package/node_modules/pngjs/lib/crc.js +40 -0
- package/node_modules/pngjs/lib/filter-pack.js +171 -0
- package/node_modules/pngjs/lib/filter-parse-async.js +24 -0
- package/node_modules/pngjs/lib/filter-parse-sync.js +21 -0
- package/node_modules/pngjs/lib/filter-parse.js +177 -0
- package/node_modules/pngjs/lib/format-normaliser.js +93 -0
- package/node_modules/pngjs/lib/interlace.js +95 -0
- package/node_modules/pngjs/lib/packer-async.js +50 -0
- package/node_modules/pngjs/lib/packer-sync.js +56 -0
- package/node_modules/pngjs/lib/packer.js +129 -0
- package/node_modules/pngjs/lib/paeth-predictor.js +16 -0
- package/node_modules/pngjs/lib/parser-async.js +165 -0
- package/node_modules/pngjs/lib/parser-sync.js +108 -0
- package/node_modules/pngjs/lib/parser.js +290 -0
- package/node_modules/pngjs/lib/png-sync.js +12 -0
- package/node_modules/pngjs/lib/png.js +194 -0
- package/node_modules/pngjs/lib/sync-inflate.js +168 -0
- package/node_modules/pngjs/lib/sync-reader.js +45 -0
- package/node_modules/pngjs/package.json +73 -0
- package/node_modules/qrcode/README.md +770 -0
- package/node_modules/qrcode/bin/qrcode +159 -0
- package/node_modules/qrcode/helper/to-sjis-browser.js +2 -0
- package/node_modules/qrcode/helper/to-sjis.js +105 -0
- package/node_modules/qrcode/lib/browser.js +76 -0
- package/node_modules/qrcode/lib/can-promise.js +7 -0
- package/node_modules/qrcode/lib/core/alignment-pattern.js +83 -0
- package/node_modules/qrcode/lib/core/alphanumeric-data.js +59 -0
- package/node_modules/qrcode/lib/core/bit-buffer.js +37 -0
- package/node_modules/qrcode/lib/core/bit-matrix.js +65 -0
- package/node_modules/qrcode/lib/core/byte-data.js +30 -0
- package/node_modules/qrcode/lib/core/error-correction-code.js +135 -0
- package/node_modules/qrcode/lib/core/error-correction-level.js +50 -0
- package/node_modules/qrcode/lib/core/finder-pattern.js +22 -0
- package/node_modules/qrcode/lib/core/format-info.js +29 -0
- package/node_modules/qrcode/lib/core/galois-field.js +69 -0
- package/node_modules/qrcode/lib/core/kanji-data.js +54 -0
- package/node_modules/qrcode/lib/core/mask-pattern.js +234 -0
- package/node_modules/qrcode/lib/core/mode.js +167 -0
- package/node_modules/qrcode/lib/core/numeric-data.js +43 -0
- package/node_modules/qrcode/lib/core/polynomial.js +62 -0
- package/node_modules/qrcode/lib/core/qrcode.js +495 -0
- package/node_modules/qrcode/lib/core/reed-solomon-encoder.js +56 -0
- package/node_modules/qrcode/lib/core/regex.js +31 -0
- package/node_modules/qrcode/lib/core/segments.js +330 -0
- package/node_modules/qrcode/lib/core/utils.js +63 -0
- package/node_modules/qrcode/lib/core/version-check.js +9 -0
- package/node_modules/qrcode/lib/core/version.js +163 -0
- package/node_modules/qrcode/lib/index.js +12 -0
- package/node_modules/qrcode/lib/renderer/canvas.js +63 -0
- package/node_modules/qrcode/lib/renderer/png.js +78 -0
- package/node_modules/qrcode/lib/renderer/svg-tag.js +81 -0
- package/node_modules/qrcode/lib/renderer/svg.js +19 -0
- package/node_modules/qrcode/lib/renderer/terminal/terminal-small.js +85 -0
- package/node_modules/qrcode/lib/renderer/terminal/terminal.js +49 -0
- package/node_modules/qrcode/lib/renderer/terminal.js +9 -0
- package/node_modules/qrcode/lib/renderer/utf8.js +71 -0
- package/node_modules/qrcode/lib/renderer/utils.js +99 -0
- package/node_modules/qrcode/lib/server.js +138 -0
- package/node_modules/qrcode/license +10 -0
- package/node_modules/qrcode/package.json +77 -0
- package/node_modules/require-directory/.jshintrc +67 -0
- package/node_modules/require-directory/.npmignore +1 -0
- package/node_modules/require-directory/.travis.yml +3 -0
- package/node_modules/require-directory/LICENSE +22 -0
- package/node_modules/require-directory/README.markdown +184 -0
- package/node_modules/require-directory/index.js +86 -0
- package/node_modules/require-directory/package.json +40 -0
- package/node_modules/require-main-filename/CHANGELOG.md +26 -0
- package/node_modules/require-main-filename/LICENSE.txt +14 -0
- package/node_modules/require-main-filename/README.md +26 -0
- package/node_modules/require-main-filename/index.js +18 -0
- package/node_modules/require-main-filename/package.json +35 -0
- package/node_modules/set-blocking/CHANGELOG.md +26 -0
- package/node_modules/set-blocking/LICENSE.txt +14 -0
- package/node_modules/set-blocking/README.md +31 -0
- package/node_modules/set-blocking/index.js +7 -0
- package/node_modules/set-blocking/package.json +42 -0
- package/node_modules/string-width/index.d.ts +29 -0
- package/node_modules/string-width/index.js +47 -0
- package/node_modules/string-width/license +9 -0
- package/node_modules/string-width/package.json +56 -0
- package/node_modules/string-width/readme.md +50 -0
- package/node_modules/strip-ansi/index.d.ts +17 -0
- package/node_modules/strip-ansi/index.js +4 -0
- package/node_modules/strip-ansi/license +9 -0
- package/node_modules/strip-ansi/package.json +54 -0
- package/node_modules/strip-ansi/readme.md +46 -0
- package/node_modules/which-module/LICENSE +13 -0
- package/node_modules/which-module/README.md +58 -0
- package/node_modules/which-module/index.js +9 -0
- package/node_modules/which-module/package.json +41 -0
- package/node_modules/wrap-ansi/index.js +186 -0
- package/node_modules/wrap-ansi/license +9 -0
- package/node_modules/wrap-ansi/package.json +61 -0
- package/node_modules/wrap-ansi/readme.md +97 -0
- package/node_modules/y18n/CHANGELOG.md +35 -0
- package/node_modules/y18n/LICENSE +13 -0
- package/node_modules/y18n/README.md +109 -0
- package/node_modules/y18n/index.js +188 -0
- package/node_modules/y18n/package.json +39 -0
- package/node_modules/yargs/CHANGELOG.md +420 -0
- package/node_modules/yargs/LICENSE +21 -0
- package/node_modules/yargs/README.md +140 -0
- package/node_modules/yargs/build/lib/apply-extends.d.ts +2 -0
- package/node_modules/yargs/build/lib/apply-extends.js +65 -0
- package/node_modules/yargs/build/lib/argsert.d.ts +2 -0
- package/node_modules/yargs/build/lib/argsert.js +65 -0
- package/node_modules/yargs/build/lib/command.d.ts +64 -0
- package/node_modules/yargs/build/lib/command.js +416 -0
- package/node_modules/yargs/build/lib/common-types.d.ts +36 -0
- package/node_modules/yargs/build/lib/common-types.js +25 -0
- package/node_modules/yargs/build/lib/completion-templates.d.ts +2 -0
- package/node_modules/yargs/build/lib/completion-templates.js +50 -0
- package/node_modules/yargs/build/lib/completion.d.ts +21 -0
- package/node_modules/yargs/build/lib/completion.js +135 -0
- package/node_modules/yargs/build/lib/is-promise.d.ts +1 -0
- package/node_modules/yargs/build/lib/is-promise.js +9 -0
- package/node_modules/yargs/build/lib/levenshtein.d.ts +1 -0
- package/node_modules/yargs/build/lib/levenshtein.js +58 -0
- package/node_modules/yargs/build/lib/middleware.d.ts +10 -0
- package/node_modules/yargs/build/lib/middleware.js +57 -0
- package/node_modules/yargs/build/lib/obj-filter.d.ts +1 -0
- package/node_modules/yargs/build/lib/obj-filter.js +14 -0
- package/node_modules/yargs/build/lib/parse-command.d.ts +11 -0
- package/node_modules/yargs/build/lib/parse-command.js +36 -0
- package/node_modules/yargs/build/lib/process-argv.d.ts +2 -0
- package/node_modules/yargs/build/lib/process-argv.js +31 -0
- package/node_modules/yargs/build/lib/usage.d.ts +49 -0
- package/node_modules/yargs/build/lib/usage.js +540 -0
- package/node_modules/yargs/build/lib/validation.d.ts +34 -0
- package/node_modules/yargs/build/lib/validation.js +330 -0
- package/node_modules/yargs/build/lib/yargs.d.ts +274 -0
- package/node_modules/yargs/build/lib/yargs.js +1190 -0
- package/node_modules/yargs/build/lib/yerror.d.ts +4 -0
- package/node_modules/yargs/build/lib/yerror.js +11 -0
- package/node_modules/yargs/index.js +40 -0
- package/node_modules/yargs/locales/be.json +46 -0
- package/node_modules/yargs/locales/de.json +46 -0
- package/node_modules/yargs/locales/en.json +51 -0
- package/node_modules/yargs/locales/es.json +46 -0
- package/node_modules/yargs/locales/fi.json +49 -0
- package/node_modules/yargs/locales/fr.json +53 -0
- package/node_modules/yargs/locales/hi.json +49 -0
- package/node_modules/yargs/locales/hu.json +46 -0
- package/node_modules/yargs/locales/id.json +50 -0
- package/node_modules/yargs/locales/it.json +46 -0
- package/node_modules/yargs/locales/ja.json +51 -0
- package/node_modules/yargs/locales/ko.json +49 -0
- package/node_modules/yargs/locales/nb.json +44 -0
- package/node_modules/yargs/locales/nl.json +49 -0
- package/node_modules/yargs/locales/nn.json +44 -0
- package/node_modules/yargs/locales/pirate.json +13 -0
- package/node_modules/yargs/locales/pl.json +49 -0
- package/node_modules/yargs/locales/pt.json +45 -0
- package/node_modules/yargs/locales/pt_BR.json +48 -0
- package/node_modules/yargs/locales/ru.json +46 -0
- package/node_modules/yargs/locales/th.json +46 -0
- package/node_modules/yargs/locales/tr.json +48 -0
- package/node_modules/yargs/locales/zh_CN.json +48 -0
- package/node_modules/yargs/locales/zh_TW.json +47 -0
- package/node_modules/yargs/package.json +92 -0
- package/node_modules/yargs/yargs.js +14 -0
- package/node_modules/yargs-parser/CHANGELOG.md +601 -0
- package/node_modules/yargs-parser/LICENSE.txt +14 -0
- package/node_modules/yargs-parser/README.md +449 -0
- package/node_modules/yargs-parser/index.js +1032 -0
- package/node_modules/yargs-parser/lib/tokenize-arg-string.js +40 -0
- package/node_modules/yargs-parser/package.json +46 -0
- package/package.json +65 -12
- package/DSH-Remote-v3.2.2.apk +0 -4
- package/manifest.json +0 -21
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
// 可回滚包的独立运行闭包。内容标识只覆盖插件代码;本模块的 digest 覆盖整个冻结包。
|
|
2
|
+
// 根只复制 package.json + lib,依赖按 Node 实际解析的包身份冻结(不复制 profile/secret)。
|
|
3
|
+
//
|
|
4
|
+
// App 载体排除(设计稿 §5.5):载体是随包的**数据依赖**(72MB APK),不是运行依赖。
|
|
5
|
+
// 只有「根包显式声明 + 身份校验通过」的载体在**建图阶段**(要求可执行入口之前)被跳过;
|
|
6
|
+
// 建图、冻结复制、冻结后闭包校验、快照复用与监督器恢复都走同一条判定
|
|
7
|
+
// `appCarrierExclusion`,普通依赖自报 `dsh.appCarrier` 不构成豁免。
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import {
|
|
10
|
+
chmodSync, copyFileSync, lstatSync, mkdirSync, mkdtempSync, readFileSync,
|
|
11
|
+
readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
15
|
+
import { spawnSync } from "node:child_process";
|
|
16
|
+
import { appCarrierExclusion } from "./app-carrier.js";
|
|
17
|
+
|
|
18
|
+
export const RUNTIME_COMPLETE_MARKER = ".complete";
|
|
19
|
+
const MARKER_SCHEMA = 1;
|
|
20
|
+
|
|
21
|
+
function contained(root, target) {
|
|
22
|
+
const rel = relative(root, target);
|
|
23
|
+
return rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 目标可能尚不存在;先解析已有祖先的 symlink,禁止借路径别名把 staging 写回源。
|
|
27
|
+
function canonicalDestination(destination) {
|
|
28
|
+
const missing = [basename(destination)];
|
|
29
|
+
let parent = dirname(destination);
|
|
30
|
+
while (true) {
|
|
31
|
+
try { return join(realpathSync(parent), ...missing); }
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error.code !== "ENOENT") throw error;
|
|
34
|
+
try {
|
|
35
|
+
lstatSync(parent); // 已有 broken symlink 不是可创建的缺失祖先。
|
|
36
|
+
throw new Error("snapshot-destination-broken-ancestor");
|
|
37
|
+
} catch (entryError) { if (entryError.code !== "ENOENT") throw entryError; }
|
|
38
|
+
missing.unshift(basename(parent));
|
|
39
|
+
parent = dirname(parent);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 全新的 Node 进程:无继承的 loader/NODE_OPTIONS、NODE_PATH、HOME 或 module cache。
|
|
45
|
+
// 静态 dependencies 图查缺失的 lazy 依赖;加载钩子校验实际加载图(含未声明 require/import)。
|
|
46
|
+
const WORKER = String.raw`
|
|
47
|
+
import fs from 'node:fs';
|
|
48
|
+
import path from 'node:path';
|
|
49
|
+
import Module, { createRequire, isBuiltin } from 'node:module';
|
|
50
|
+
import { fileURLToPath } from 'node:url';
|
|
51
|
+
const input = JSON.parse(fs.readFileSync(0, 'utf8'));
|
|
52
|
+
const root = fs.realpathSync(input.root);
|
|
53
|
+
const loaded = new Set();
|
|
54
|
+
const contains = (target) => {
|
|
55
|
+
const rel = path.relative(root, target);
|
|
56
|
+
return rel === '' || (rel !== '..' && !rel.startsWith('..' + path.sep) && !path.isAbsolute(rel));
|
|
57
|
+
};
|
|
58
|
+
const readPackage = (dir) => JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
|
|
59
|
+
function binding(req, name, optional = false) {
|
|
60
|
+
if (!/^(?:@[^/\s]+\/)?[^./\s][^/\s]*$/.test(name) || isBuiltin(name)) throw new Error(name + ':invalid-dependency-name');
|
|
61
|
+
let source;
|
|
62
|
+
for (const lookup of req.resolve.paths(name) ?? []) {
|
|
63
|
+
const candidate = path.join(lookup, name);
|
|
64
|
+
// scoped 容器本身的 broken symlink 也不能被上层同名包兜底。
|
|
65
|
+
if (name.startsWith('@')) {
|
|
66
|
+
const scope = path.join(lookup, name.split('/')[0]);
|
|
67
|
+
let scopeStat;
|
|
68
|
+
try { scopeStat = fs.lstatSync(scope); }
|
|
69
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
70
|
+
if (scopeStat?.isSymbolicLink()) {
|
|
71
|
+
try { fs.realpathSync(scope); }
|
|
72
|
+
catch (error) { throw new Error(name + ':broken-required-link(' + error.code + ')'); }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
try { fs.lstatSync(candidate); }
|
|
76
|
+
catch (error) { if (error.code === 'ENOENT') continue; throw error; }
|
|
77
|
+
try { source = fs.realpathSync(candidate); }
|
|
78
|
+
catch (error) { throw new Error(name + ':broken-required-link(' + error.code + ')'); }
|
|
79
|
+
if (!fs.statSync(source).isDirectory()) throw new Error(name + ':dependency-not-directory');
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
if (!source) {
|
|
83
|
+
if (optional) return null;
|
|
84
|
+
throw new Error(name + ':无法解析(MODULE_NOT_FOUND)');
|
|
85
|
+
}
|
|
86
|
+
let entry;
|
|
87
|
+
try { entry = fs.realpathSync(req.resolve(name)); }
|
|
88
|
+
catch (error) { throw new Error(name + ':无法解析(' + error.code + ':' + error.message + ')'); }
|
|
89
|
+
if (input.mode === 'verify' && (!contains(source) || !contains(entry))) {
|
|
90
|
+
throw new Error(name + ':解析到解包目录之外(' + entry + ')');
|
|
91
|
+
}
|
|
92
|
+
return { source, entry };
|
|
93
|
+
}
|
|
94
|
+
function graph() {
|
|
95
|
+
const nodes = [{ source: root, dependencies: Object.create(null) }];
|
|
96
|
+
const ids = new Map([[root, 0]]);
|
|
97
|
+
// 只有父进程用「根包声明 + 完整身份校验」确认过的载体别名才会出现在 input.carrier。
|
|
98
|
+
// 必须在要求可执行入口(binding 的 req.resolve)之前跳过它:载体是数据包,没有 main/exports。
|
|
99
|
+
const excludedAlias = typeof input.carrier?.alias === 'string' ? input.carrier.alias : null;
|
|
100
|
+
for (let index = 0; index < nodes.length; index++) {
|
|
101
|
+
if (nodes.length > 4096) throw new Error('runtime-closure-too-large');
|
|
102
|
+
const node = nodes[index];
|
|
103
|
+
const pkg = readPackage(node.source);
|
|
104
|
+
node.name = pkg.name ?? '';
|
|
105
|
+
const req = createRequire(path.join(node.source, 'package.json'));
|
|
106
|
+
const deps = { ...pkg.dependencies, ...pkg.optionalDependencies };
|
|
107
|
+
for (const name of Object.keys(deps).sort()) {
|
|
108
|
+
// 仅根包自己的载体别名豁免;其他包即使同名依赖也照常进入闭包。
|
|
109
|
+
if (index === 0 && excludedAlias !== null && name === excludedAlias) continue;
|
|
110
|
+
const found = binding(req, name, Object.hasOwn(pkg.optionalDependencies ?? {}, name));
|
|
111
|
+
if (!found) continue;
|
|
112
|
+
let id = ids.get(found.source);
|
|
113
|
+
if (id === undefined) {
|
|
114
|
+
id = nodes.length;
|
|
115
|
+
ids.set(found.source, id);
|
|
116
|
+
nodes.push({ source: found.source, dependencies: Object.create(null) });
|
|
117
|
+
}
|
|
118
|
+
node.dependencies[name] = id;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return nodes;
|
|
122
|
+
}
|
|
123
|
+
function checkLoad(filename) {
|
|
124
|
+
if (isBuiltin(filename)) return;
|
|
125
|
+
const real = fs.realpathSync(filename);
|
|
126
|
+
if (!contains(real)) throw new Error('runtime-load-outside-package(' + real + ')');
|
|
127
|
+
loaded.add(real);
|
|
128
|
+
}
|
|
129
|
+
async function run() {
|
|
130
|
+
const nodes = graph();
|
|
131
|
+
if (input.mode === 'graph') return { ok: true, nodes };
|
|
132
|
+
if (typeof Module.registerHooks !== 'function') throw new Error('runtime-closure-verification-requires-Node-registerHooks');
|
|
133
|
+
const original = Module._resolveFilename;
|
|
134
|
+
Module._resolveFilename = function(...args) {
|
|
135
|
+
const filename = original.apply(this, args);
|
|
136
|
+
checkLoad(filename);
|
|
137
|
+
return filename;
|
|
138
|
+
};
|
|
139
|
+
// 新版 Node 的同步 hooks 同时覆盖传递 ESM import 和 CJS require。
|
|
140
|
+
Module.registerHooks({ resolve(specifier, context, nextResolve) {
|
|
141
|
+
const result = nextResolve(specifier, context);
|
|
142
|
+
if (result.url.startsWith('file:')) checkLoad(fileURLToPath(result.url));
|
|
143
|
+
else if (!result.url.startsWith('node:')) throw new Error('runtime-load-unsupported-url(' + result.url + ')');
|
|
144
|
+
return result;
|
|
145
|
+
} });
|
|
146
|
+
for (const node of nodes) {
|
|
147
|
+
const req = createRequire(path.join(node.source, 'package.json'));
|
|
148
|
+
for (const [name, id] of Object.entries(node.dependencies)) {
|
|
149
|
+
try {
|
|
150
|
+
const dependency = req(name);
|
|
151
|
+
if (name === 'qrcode' || nodes[id].name === 'qrcode') {
|
|
152
|
+
if (typeof dependency.toBuffer !== 'function') throw new Error('qrcode.toBuffer missing');
|
|
153
|
+
const png = await dependency.toBuffer('dsh-runtime-closure-smoke', { type: 'png', width: 64, margin: 1, errorCorrectionLevel: 'M' });
|
|
154
|
+
if (!Buffer.isBuffer(png) || !png.subarray(0, 8).equals(Buffer.from([137,80,78,71,13,10,26,10]))) {
|
|
155
|
+
throw new Error('qrcode.toBuffer did not produce PNG');
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
} catch (error) { throw new Error(name + ':加载失败(' + error.message + ')'); }
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return { ok: true, dependencies: Object.keys(nodes[0].dependencies), packageCount: nodes.length, loadedFiles: [...loaded].sort() };
|
|
162
|
+
}
|
|
163
|
+
try { process.stdout.write('\n__DSH_CLOSURE_RESULT__' + JSON.stringify(await run()) + '\n'); }
|
|
164
|
+
catch (error) { process.stdout.write('\n__DSH_CLOSURE_RESULT__' + JSON.stringify({ ok: false, error: error.message }) + '\n'); process.exitCode = 1; }
|
|
165
|
+
`;
|
|
166
|
+
|
|
167
|
+
function cleanupTemporary(dir, parent) {
|
|
168
|
+
// 删除前必须核验实际绝对目标:只能是本次 mkdtemp 分配的直接子目录。
|
|
169
|
+
if (!isAbsolute(dir) || dirname(dir) !== parent || !basename(dir).startsWith(".dsh-runtime-")) {
|
|
170
|
+
throw new Error("unsafe-temporary-cleanup");
|
|
171
|
+
}
|
|
172
|
+
rmSync(dir, { recursive: true, force: true });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function runWorker(root, mode, carrier = null) {
|
|
176
|
+
const parent = realpathSync(tmpdir());
|
|
177
|
+
const scratch = mkdtempSync(join(parent, ".dsh-runtime-check-"));
|
|
178
|
+
try {
|
|
179
|
+
const child = spawnSync(process.execPath, ["--no-global-search-paths", "--input-type=module", "-e", WORKER], {
|
|
180
|
+
input: JSON.stringify({ root, mode, carrier }), encoding: "utf8", cwd: scratch,
|
|
181
|
+
env: { PATH: process.env.PATH ?? "", HOME: scratch, TMPDIR: scratch, TEMP: scratch, TMP: scratch, LANG: "C" },
|
|
182
|
+
timeout: 15_000, maxBuffer: 4 * 1024 * 1024,
|
|
183
|
+
});
|
|
184
|
+
if (child.error) return { ok: false, error: `runtime-check-failed: ${child.error.message}` };
|
|
185
|
+
const resultLine = child.stdout.split("\n").filter((line) => line.startsWith("__DSH_CLOSURE_RESULT__")).at(-1);
|
|
186
|
+
if (!resultLine) return { ok: false, error: `runtime-check-no-result: exit=${child.status} ${child.stderr.trim()}` };
|
|
187
|
+
const result = JSON.parse(resultLine.slice("__DSH_CLOSURE_RESULT__".length));
|
|
188
|
+
if (child.status !== 0 && result.ok) return { ok: false, error: `runtime-check-exit-${child.status}` };
|
|
189
|
+
return result;
|
|
190
|
+
} finally {
|
|
191
|
+
cleanupTemporary(scratch, parent);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function inventory(packageRoot) {
|
|
196
|
+
const root = realpathSync(packageRoot);
|
|
197
|
+
const records = [];
|
|
198
|
+
const visit = (dir, rel = "") => {
|
|
199
|
+
for (const name of readdirSync(dir).sort()) {
|
|
200
|
+
if (rel === "" && name === RUNTIME_COMPLETE_MARKER) continue;
|
|
201
|
+
const full = join(dir, name);
|
|
202
|
+
const child = rel ? `${rel}/${name}` : name;
|
|
203
|
+
const st = lstatSync(full);
|
|
204
|
+
if (st.isSymbolicLink()) {
|
|
205
|
+
const real = realpathSync(full); // broken 链接也必须失败,不允许 skip。
|
|
206
|
+
if (!contained(root, real)) throw new Error(`symlink-outside-package: ${child}`);
|
|
207
|
+
// 只记录链接,不递归,可覆盖循环依赖;目标的文件另有独立记录。
|
|
208
|
+
records.push([child, "link", readlinkSync(full)]);
|
|
209
|
+
} else if (st.isDirectory()) {
|
|
210
|
+
records.push([child, "dir"]);
|
|
211
|
+
visit(full, child);
|
|
212
|
+
} else if (st.isFile()) {
|
|
213
|
+
records.push([child, "file", st.mode & 0o777, createHash("sha256").update(readFileSync(full)).digest("hex")]);
|
|
214
|
+
} else throw new Error(`unsupported-package-entry: ${child}`);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
visit(root);
|
|
218
|
+
const digest = createHash("sha256").update("dsh-runtime-closure/v1\n").update(JSON.stringify(records)).digest("hex");
|
|
219
|
+
return { digest, entries: records.length };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 载体排除的单一判定点(建图/冻结/冻结后校验/复用恢复共用):
|
|
223
|
+
// - 通过身份校验 → 返回可跳过的别名;
|
|
224
|
+
// - 根包声明了、作用域里也确实有候选,但候选身份不符/多义 → fail-closed,带稳定 reason;
|
|
225
|
+
// - 未声明或未安装 → 正常(可选依赖缺失不阻塞闭包)。
|
|
226
|
+
// 普通依赖自报 dsh.appCarrier 不会走到这里,也不会被豁免。
|
|
227
|
+
function carrierExclusion(root) {
|
|
228
|
+
const exclusion = appCarrierExclusion({ root });
|
|
229
|
+
if (exclusion?.exclude === true && typeof exclusion.alias === "string") {
|
|
230
|
+
return { ok: true, carrier: { alias: exclusion.alias } };
|
|
231
|
+
}
|
|
232
|
+
if (exclusion?.hard === true) return { ok: false, error: `app-carrier-invalid: ${exclusion.reason}` };
|
|
233
|
+
return { ok: true, carrier: null };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* 无 marker 也可验证发行 tgz:全部声明依赖与 smoke 实际加载文件均在 packageRoot 内。
|
|
238
|
+
* qrcode.toBuffer(PNG) 覆盖 lazy API;CJS + ESM require/import 均做 realpath containment。
|
|
239
|
+
* 需要支持 module.registerHooks 的 Node(22.15+/23.5+);不支持时 fail-closed,不降为只核 CJS。
|
|
240
|
+
* 根包显式声明且身份校验通过的 App 载体不进入闭包(数据依赖,不是运行依赖)。
|
|
241
|
+
*/
|
|
242
|
+
export function verifyRuntimeClosure(packageRoot) {
|
|
243
|
+
try {
|
|
244
|
+
if (!statSync(join(packageRoot, "lib")).isDirectory()) throw new Error("package-lib-missing");
|
|
245
|
+
inventory(packageRoot); // 任何树内外链/broken 链接均拒绝。
|
|
246
|
+
const root = realpathSync(packageRoot);
|
|
247
|
+
const exclusion = carrierExclusion(root);
|
|
248
|
+
if (!exclusion.ok) return { ok: false, error: exclusion.error };
|
|
249
|
+
return runWorker(root, "verify", exclusion.carrier);
|
|
250
|
+
} catch (error) {
|
|
251
|
+
return { ok: false, error: String(error.message ?? error) };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** digest、marker schema、完整加载图全过,才可以复用快照。旧的时间戳 marker 一律拒绝。 */
|
|
256
|
+
export function verifyFrozenPackage(packageRoot) {
|
|
257
|
+
try {
|
|
258
|
+
const markerPath = join(packageRoot, RUNTIME_COMPLETE_MARKER);
|
|
259
|
+
if (!lstatSync(markerPath).isFile()) throw new Error("snapshot-marker-not-file");
|
|
260
|
+
const marker = JSON.parse(readFileSync(markerPath, "utf8"));
|
|
261
|
+
if (marker.schema !== MARKER_SCHEMA || marker.algorithm !== "sha256" || !/^[a-f0-9]{64}$/.test(marker.digest ?? "")) {
|
|
262
|
+
throw new Error("snapshot-marker-invalid");
|
|
263
|
+
}
|
|
264
|
+
const actual = inventory(packageRoot);
|
|
265
|
+
if (marker.digest !== actual.digest || marker.entries !== actual.entries) throw new Error("snapshot-integrity-mismatch");
|
|
266
|
+
const runtime = verifyRuntimeClosure(packageRoot);
|
|
267
|
+
return runtime.ok ? { ...runtime, digest: actual.digest } : runtime;
|
|
268
|
+
} catch (error) {
|
|
269
|
+
return { ok: false, error: String(error.message ?? error) };
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function frozenPackageReady(packageRoot) {
|
|
274
|
+
return verifyFrozenPackage(packageRoot).ok;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function copyPayload(source, destination, copyFile, ancestors = new Set()) {
|
|
278
|
+
const real = realpathSync(source); // 必需链接缺失必须 throw。
|
|
279
|
+
const st = statSync(real);
|
|
280
|
+
if (st.isFile()) {
|
|
281
|
+
const result = copyFile(real, destination);
|
|
282
|
+
if (result?.ok === false) throw new Error(result.error ?? "copy-file-failed");
|
|
283
|
+
chmodSync(destination, st.mode & 0o777);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (!st.isDirectory()) throw new Error(`unsupported-source-entry: ${source}`);
|
|
287
|
+
if (ancestors.has(real)) throw new Error(`source-symlink-cycle: ${source}`);
|
|
288
|
+
const next = new Set(ancestors).add(real);
|
|
289
|
+
mkdirSync(destination, { recursive: true });
|
|
290
|
+
for (const name of readdirSync(real).sort()) {
|
|
291
|
+
// 不复制安装布局 / VCS 元数据;由真实解析的图重新构造 node_modules。
|
|
292
|
+
if (name === "node_modules" || name === ".git") continue;
|
|
293
|
+
copyPayload(join(real, name), join(destination, name), copyFile, next);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* 同步、fail-closed、不可覆盖的快照发布。
|
|
299
|
+
* copyFile(src,dest) 可注入真实拷贝故障;throw 或 {ok:false,error} 都拒绝,验证前无 .complete。
|
|
300
|
+
* destination 必须尚不存在(损坏的旧目录由调用方拒绝使用,不能当可复用快照)。
|
|
301
|
+
*/
|
|
302
|
+
export function freezePackage({ sourceRoot, destination, copyFile = copyFileSync } = {}) {
|
|
303
|
+
let staging;
|
|
304
|
+
let parent;
|
|
305
|
+
try {
|
|
306
|
+
const source = realpathSync(sourceRoot);
|
|
307
|
+
const dest = canonicalDestination(resolve(destination));
|
|
308
|
+
if (contained(source, dest) || contained(dest, source)) throw new Error("snapshot-source-destination-overlap");
|
|
309
|
+
try { lstatSync(dest); throw new Error("snapshot-destination-exists"); }
|
|
310
|
+
catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
311
|
+
// 先判定载体排除(根包声明 + 身份校验),再建图:载体在建图阶段就被跳过。
|
|
312
|
+
const carrier = carrierExclusion(source);
|
|
313
|
+
if (!carrier.ok) throw new Error(carrier.error);
|
|
314
|
+
const graph = runWorker(source, "graph", carrier.carrier);
|
|
315
|
+
if (!graph.ok) return graph;
|
|
316
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
317
|
+
parent = realpathSync(dirname(dest));
|
|
318
|
+
staging = mkdtempSync(join(parent, ".dsh-runtime-snapshot-"));
|
|
319
|
+
copyPayload(join(source, "package.json"), join(staging, "package.json"), copyFile);
|
|
320
|
+
copyPayload(join(source, "lib"), join(staging, "lib"), copyFile);
|
|
321
|
+
const targets = graph.nodes.map((_, id) => id === 0 ? staging : join(staging, "node_modules/.runtime-deps", String(id)));
|
|
322
|
+
for (let id = 1; id < graph.nodes.length; id++) copyPayload(graph.nodes[id].source, targets[id], copyFile);
|
|
323
|
+
for (let id = 0; id < graph.nodes.length; id++) {
|
|
324
|
+
for (const [name, dependencyId] of Object.entries(graph.nodes[id].dependencies)) {
|
|
325
|
+
const link = join(targets[id], "node_modules", name);
|
|
326
|
+
mkdirSync(dirname(link), { recursive: true });
|
|
327
|
+
symlinkSync(relative(dirname(link), targets[dependencyId]), link, "dir");
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const runtime = verifyRuntimeClosure(staging);
|
|
331
|
+
if (!runtime.ok) throw new Error(runtime.error);
|
|
332
|
+
const snapshot = inventory(staging);
|
|
333
|
+
writeFileSync(join(staging, RUNTIME_COMPLETE_MARKER), JSON.stringify({ schema: MARKER_SCHEMA, algorithm: "sha256", ...snapshot }) + "\n", { flag: "wx" });
|
|
334
|
+
// 同一父目录内 rename,一次发布已验证树。绝不先删已有目标。
|
|
335
|
+
try { lstatSync(dest); throw new Error("snapshot-destination-exists"); }
|
|
336
|
+
catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
337
|
+
renameSync(staging, dest);
|
|
338
|
+
staging = undefined;
|
|
339
|
+
return { ok: true, dir: dest, digest: snapshot.digest, carrierExcluded: carrier.carrier !== null };
|
|
340
|
+
} catch (error) {
|
|
341
|
+
return { ok: false, error: `snapshot-failed: ${error.message ?? error}` };
|
|
342
|
+
} finally {
|
|
343
|
+
if (staging) cleanupTemporary(staging, parent);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// issue #20 第 2 步(实现要点 1):**revision 失效的持久化标题缓存**。
|
|
2
|
+
//
|
|
3
|
+
// 背景(见 issue #20 的 note 702 / 793):
|
|
4
|
+
// - `/sessions` 在响应前同步折叠**全部休眠会话**的标题,而折叠要完整读取每个会话日志
|
|
5
|
+
// (追加式多帧 zstd)——隔离实测冷缓存 68.2 s、生产 600 s 未返回。
|
|
6
|
+
// - 内核 `sessionQuery.observeSession(id)` 返回的 lease 带 `revision`(来自**不读日志**的
|
|
7
|
+
// `persistence.stat()`)。revision 未变即意味着日志没变 → **可以零日志读取地复用标题**。
|
|
8
|
+
// - 但 `stat()` 本身也不便宜(实测 61.9 ms/会话 → 全量 354 个 21.9 s),所以本缓存
|
|
9
|
+
// **必须同时有界**:条目数有界、每次请求的发散量有界(由调用方的时间预算控制)。
|
|
10
|
+
//
|
|
11
|
+
// 本模块只负责"存与取",不做调度——预算与 single-flight 由调用方掌握。
|
|
12
|
+
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { dirname } from "node:path";
|
|
15
|
+
|
|
16
|
+
/** 默认条目上限:约 1000 个会话足够覆盖常见语料,且文件体积可控(~100 KB 量级)。 */
|
|
17
|
+
export const DEFAULT_MAX_ENTRIES = 1000;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {object} options
|
|
21
|
+
* @param {string} options.file 落盘路径(`~/.dsh/mobile-remote/session-titles.json`)
|
|
22
|
+
* @param {number} [options.maxEntries] 条目上限(超出按最久未更新淘汰)
|
|
23
|
+
* @param {() => number} [options.now] 时间源(测试可注入)
|
|
24
|
+
*/
|
|
25
|
+
export const createTitleCache = ({ file, maxEntries = DEFAULT_MAX_ENTRIES, now = Date.now }) => {
|
|
26
|
+
/** sessionId -> { title: string|null, revision: string, at: number } */
|
|
27
|
+
const entries = new Map();
|
|
28
|
+
let dirty = false;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 读盘并替换当前内存快照。只接受 version 1;缺失/损坏/未知格式一律降级为空缓存,
|
|
32
|
+
* 缓存坏了不该让列表挂掉,也不能把不可信字段当成已确认的标题事实。
|
|
33
|
+
*/
|
|
34
|
+
const load = () => {
|
|
35
|
+
entries.clear();
|
|
36
|
+
dirty = false;
|
|
37
|
+
try {
|
|
38
|
+
if (!existsSync(file)) return;
|
|
39
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
40
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || parsed.version !== 1) return;
|
|
41
|
+
const raw = parsed.entries;
|
|
42
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
|
|
43
|
+
for (const [id, value] of Object.entries(raw)) {
|
|
44
|
+
if (id === "") continue;
|
|
45
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
46
|
+
if (typeof value.revision !== "string" || value.revision === "") continue;
|
|
47
|
+
// 只有显式 null 才代表“确实没有标题”;缺失或其它类型必须回源,不能伪造命中。
|
|
48
|
+
if (value.title !== null && typeof value.title !== "string") continue;
|
|
49
|
+
const title = value.title === "" ? null : value.title;
|
|
50
|
+
entries.set(id, { title, revision: value.revision, at: typeof value.at === "number" ? value.at : 0 });
|
|
51
|
+
}
|
|
52
|
+
prune();
|
|
53
|
+
} catch {
|
|
54
|
+
entries.clear();
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 取缓存标题。
|
|
60
|
+
* @returns `{ hit: true, title }` 表示 **revision 相同、可零日志读取地复用**;
|
|
61
|
+
* `{ hit: false }` 表示需要回源折叠。
|
|
62
|
+
*/
|
|
63
|
+
const get = (id, revision) => {
|
|
64
|
+
if (typeof id !== "string" || id === "" || typeof revision !== "string" || revision === "") {
|
|
65
|
+
return { hit: false };
|
|
66
|
+
}
|
|
67
|
+
const entry = entries.get(id);
|
|
68
|
+
if (!entry || entry.revision !== revision) return { hit: false };
|
|
69
|
+
// 命中也要刷新时间戳:淘汰按"最久未更新",命中说明这个会话仍活跃。
|
|
70
|
+
entry.at = now();
|
|
71
|
+
dirty = true;
|
|
72
|
+
return { hit: true, title: entry.title };
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** 写入(title 允许为 null——"确实没有标题"同样是可缓存的事实)。 */
|
|
76
|
+
const set = (id, revision, title) => {
|
|
77
|
+
if (typeof id !== "string" || id === "" || typeof revision !== "string" || revision === "") return;
|
|
78
|
+
entries.set(id, {
|
|
79
|
+
title: typeof title === "string" && title !== "" ? title : null,
|
|
80
|
+
revision,
|
|
81
|
+
at: now(),
|
|
82
|
+
});
|
|
83
|
+
dirty = true;
|
|
84
|
+
prune();
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/** 按 `at` 淘汰到上限以内。 */
|
|
88
|
+
const prune = () => {
|
|
89
|
+
if (entries.size <= maxEntries) return;
|
|
90
|
+
const sorted = [...entries.entries()].sort((a, b) => a[1].at - b[1].at);
|
|
91
|
+
const excess = entries.size - maxEntries;
|
|
92
|
+
for (let i = 0; i < excess; i += 1) {
|
|
93
|
+
entries.delete(sorted[i][0]);
|
|
94
|
+
dirty = true;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** 原子落盘(临时文件 + rename)。没有变更时是空操作。 */
|
|
99
|
+
const flush = () => {
|
|
100
|
+
if (!dirty) return false;
|
|
101
|
+
try {
|
|
102
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
103
|
+
const tmp = `${file}.tmp`;
|
|
104
|
+
writeFileSync(tmp, JSON.stringify({ version: 1, entries: Object.fromEntries(entries) }), "utf8");
|
|
105
|
+
renameSync(tmp, file);
|
|
106
|
+
dirty = false;
|
|
107
|
+
return true;
|
|
108
|
+
} catch {
|
|
109
|
+
// 落盘失败不该影响本次响应:内存缓存仍然有效。
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
load,
|
|
116
|
+
get,
|
|
117
|
+
set,
|
|
118
|
+
flush,
|
|
119
|
+
size: () => entries.size,
|
|
120
|
+
/** 仅测试用:直接观察某条目。 */
|
|
121
|
+
peek: (id) => entries.get(id),
|
|
122
|
+
};
|
|
123
|
+
};
|