sourcemonkey 0.0.0-stage → 0.5.0
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/LICENSE +21 -0
- package/README.md +87 -2
- package/dist/bytes.js +37 -0
- package/dist/channel-placeholder.js +5 -0
- package/dist/collection-scan.js +17 -0
- package/dist/collections.html +353 -0
- package/dist/compare.html +358 -0
- package/dist/composed-source.js +205 -0
- package/dist/connect-prompt.html +47 -0
- package/dist/debug/icons.html +211 -0
- package/dist/debug/storage.html +163 -0
- package/dist/debug-log.js +75 -0
- package/dist/diagnostic-location.js +64 -0
- package/dist/eval-hook.js +308 -0
- package/dist/extension/background.js +5263 -0
- package/dist/extension/channel.js +133 -0
- package/dist/extension/code-links.js +187 -0
- package/dist/extension/code-tabs.js +460 -0
- package/dist/extension/collection-dialog.js +619 -0
- package/dist/extension/collection-display.js +322 -0
- package/dist/extension/collections.js +2905 -0
- package/dist/extension/compare.js +867 -0
- package/dist/extension/concurrency.js +186 -0
- package/dist/extension/connect-grants.js +109 -0
- package/dist/extension/connect-prompt-page.js +70 -0
- package/dist/extension/connect-prompt.js +266 -0
- package/dist/extension/copy-button.js +63 -0
- package/dist/extension/copy-dialog.js +502 -0
- package/dist/extension/copy-preference.js +66 -0
- package/dist/extension/debug/icons.js +203 -0
- package/dist/extension/debug/storage.js +365 -0
- package/dist/extension/deep-link.js +77 -0
- package/dist/extension/delete-dialog.js +432 -0
- package/dist/extension/delete-icon.js +40 -0
- package/dist/extension/details-tooltips.js +66 -0
- package/dist/extension/directories.js +679 -0
- package/dist/extension/export-backup.js +407 -0
- package/dist/extension/export-dialog.js +142 -0
- package/dist/extension/file-access.js +74 -0
- package/dist/extension/file-changes.js +346 -0
- package/dist/extension/frame-info.js +85 -0
- package/dist/extension/github-provider.js +336 -0
- package/dist/extension/glyph.js +84 -0
- package/dist/extension/gm-bindings.js +601 -0
- package/dist/extension/gm-download.js +419 -0
- package/dist/extension/gm-menu.js +228 -0
- package/dist/extension/gm-notification.js +141 -0
- package/dist/extension/gm-originator-map.js +23 -0
- package/dist/extension/gm-tab-data.js +121 -0
- package/dist/extension/gm-tab.js +160 -0
- package/dist/extension/gm-url-change.js +111 -0
- package/dist/extension/gm-url-scheme.js +27 -0
- package/dist/extension/gm-values.js +176 -0
- package/dist/extension/gm-window-control.js +104 -0
- package/dist/extension/gm-xhr-headers.js +333 -0
- package/dist/extension/gm-xhr.js +584 -0
- package/dist/extension/injector.js +3145 -0
- package/dist/extension/install-capture.js +277 -0
- package/dist/extension/line-diff.js +233 -0
- package/dist/extension/local-copy.js +560 -0
- package/dist/extension/local-delete.js +276 -0
- package/dist/extension/location-kinds.js +108 -0
- package/dist/extension/log-format.js +158 -0
- package/dist/extension/managed-changes.js +361 -0
- package/dist/extension/managed-items.js +57 -0
- package/dist/extension/match-index.js +375 -0
- package/dist/extension/missing-scripts.js +66 -0
- package/dist/extension/more-menu.js +109 -0
- package/dist/extension/move-dialog.js +197 -0
- package/dist/extension/offscreen.js +37 -0
- package/dist/extension/options.js +504 -0
- package/dist/extension/page-heading.js +170 -0
- package/dist/extension/page-links.js +199 -0
- package/dist/extension/parse-check-client.js +70 -0
- package/dist/extension/parse-check.js +161 -0
- package/dist/extension/parse-sandbox-host.js +172 -0
- package/dist/extension/parse-sandbox.js +175 -0
- package/dist/extension/popup-commands-open.js +41 -0
- package/dist/extension/popup-view.js +387 -0
- package/dist/extension/popup.js +703 -0
- package/dist/extension/prism-userscript.js +53 -0
- package/dist/extension/refresh-error.js +136 -0
- package/dist/extension/refresh-targets.js +160 -0
- package/dist/extension/related-versions.js +1144 -0
- package/dist/extension/resize-observer-loop.js +19 -0
- package/dist/extension/row-cache.js +387 -0
- package/dist/extension/script-blobs.js +413 -0
- package/dist/extension/script-details.js +658 -0
- package/dist/extension/script-enablement.js +285 -0
- package/dist/extension/script-identity.js +310 -0
- package/dist/extension/script-keys.js +54 -0
- package/dist/extension/script-log-id.js +155 -0
- package/dist/extension/script-log.js +290 -0
- package/dist/extension/script-rejection.js +104 -0
- package/dist/extension/script-sites.js +158 -0
- package/dist/extension/script-store.js +3507 -0
- package/dist/extension/script-updater.js +991 -0
- package/dist/extension/script-url-hash.js +61 -0
- package/dist/extension/select-all.js +37 -0
- package/dist/extension/select-open-guard.js +90 -0
- package/dist/extension/site-icon.js +1261 -0
- package/dist/extension/sm-dev-link.js +302 -0
- package/dist/extension/solo-click.js +58 -0
- package/dist/extension/sticky-numbers.js +188 -0
- package/dist/extension/storage-breakdown.js +253 -0
- package/dist/extension/storage-scan.js +86 -0
- package/dist/extension/storage-usage.js +194 -0
- package/dist/extension/structural-keys.js +29 -0
- package/dist/extension/sw-teardown.js +36 -0
- package/dist/extension/tab-status.js +403 -0
- package/dist/extension/toolbar-summary.js +105 -0
- package/dist/extension/user-scripts-access.js +43 -0
- package/dist/extension/userscript.js +3672 -0
- package/dist/extension/value-store.js +204 -0
- package/dist/extension/version-probe-fetch.js +68 -0
- package/dist/extension/worker-client.js +109 -0
- package/dist/extension/worker-ops.js +171 -0
- package/dist/extension/zip-writer.js +24 -0
- package/dist/external-files.js +878 -0
- package/dist/file-loader.js +436 -0
- package/dist/file-locations.js +439 -0
- package/dist/generated-wrappers.js +5 -0
- package/dist/github-auth.js +148 -0
- package/dist/github-urls.js +46 -0
- package/dist/gm-api-catalog.js +217 -0
- package/dist/gm-connect.js +273 -0
- package/dist/gm-xhr-types.js +5 -0
- package/dist/http-read.js +142 -0
- package/dist/icons/icon-128.png +0 -0
- package/dist/icons/icon-16.png +0 -0
- package/dist/icons/icon-48.png +0 -0
- package/dist/local-file-overrides.js +558 -0
- package/dist/manifest.json +38 -0
- package/dist/markup-response.js +126 -0
- package/dist/match-targets.js +305 -0
- package/dist/menu-captions.js +22 -0
- package/dist/offscreen.html +39 -0
- package/dist/options.html +197 -0
- package/dist/parse-check-protocol.js +51 -0
- package/dist/parse-location.js +171 -0
- package/dist/parse-sandbox.html +56 -0
- package/dist/phase-timing.js +155 -0
- package/dist/popup.html +197 -0
- package/dist/public-suffixes.js +6957 -0
- package/dist/run-context.js +31 -0
- package/dist/runtime-messages.js +34 -0
- package/dist/sandboxed-page.js +35 -0
- package/dist/script-build.js +787 -0
- package/dist/script-display.js +383 -0
- package/dist/script-icon.js +43 -0
- package/dist/script-links.js +204 -0
- package/dist/script-source-url.js +214 -0
- package/dist/script-world.js +65 -0
- package/dist/sm-dev-protocol.js +84 -0
- package/dist/source-map.js +177 -0
- package/dist/stack-link.js +138 -0
- package/dist/styles.css +825 -0
- package/dist/unsafe-window-use.js +229 -0
- package/dist/url-match.js +734 -0
- package/dist/userscript-header.js +248 -0
- package/dist/userscript.html +1165 -0
- package/dist/vendor/fflate.LICENSE +21 -0
- package/dist/vendor/fflate.js +2692 -0
- package/dist/vendor/prism/prism-line-numbers.css +1 -0
- package/dist/vendor/prism/prism-line-numbers.js +1 -0
- package/dist/vendor/prism/prism.css +1 -0
- package/dist/vendor/prism/prism.js +1946 -0
- package/dist/version-compare.js +141 -0
- package/dist/version-probe.js +120 -0
- package/lib/build-info.json +1 -0
- package/lib/bytes.d.ts +3 -0
- package/lib/bytes.js +37 -0
- package/lib/channel-placeholder.d.ts +1 -0
- package/lib/channel-placeholder.js +5 -0
- package/lib/collection-scan.d.ts +8 -0
- package/lib/collection-scan.js +17 -0
- package/lib/composed-source.d.ts +28 -0
- package/lib/composed-source.js +205 -0
- package/lib/debug-log.d.ts +9 -0
- package/lib/debug-log.js +75 -0
- package/lib/diagnostic-location.d.ts +8 -0
- package/lib/diagnostic-location.js +64 -0
- package/lib/eval-hook.d.ts +34 -0
- package/lib/eval-hook.js +308 -0
- package/lib/external-files.d.ts +111 -0
- package/lib/external-files.js +878 -0
- package/lib/file-loader.d.ts +48 -0
- package/lib/file-loader.js +436 -0
- package/lib/file-locations.d.ts +44 -0
- package/lib/file-locations.js +439 -0
- package/lib/generated-wrappers.d.ts +11 -0
- package/lib/generated-wrappers.js +5 -0
- package/lib/github-auth.d.ts +12 -0
- package/lib/github-auth.js +148 -0
- package/lib/github-urls.d.ts +1 -0
- package/lib/github-urls.js +46 -0
- package/lib/gm-api-catalog.d.ts +24 -0
- package/lib/gm-api-catalog.js +217 -0
- package/lib/gm-connect.d.ts +34 -0
- package/lib/gm-connect.js +273 -0
- package/lib/gm-xhr-types.d.ts +68 -0
- package/lib/gm-xhr-types.js +5 -0
- package/lib/harness/browser-extensions.d.ts +73 -0
- package/lib/harness/browser-extensions.js +285 -0
- package/lib/harness/browser-lang.d.ts +20 -0
- package/lib/harness/browser-lang.js +86 -0
- package/lib/harness/browser-launch.d.ts +14 -0
- package/lib/harness/browser-launch.js +184 -0
- package/lib/harness/checks.d.ts +110 -0
- package/lib/harness/checks.js +321 -0
- package/lib/harness/cli.d.ts +2 -0
- package/lib/harness/cli.js +2259 -0
- package/lib/harness/command-args.d.ts +91 -0
- package/lib/harness/command-args.js +369 -0
- package/lib/harness/command-lines.d.ts +80 -0
- package/lib/harness/command-lines.js +180 -0
- package/lib/harness/command-list.d.ts +206 -0
- package/lib/harness/command-list.js +1475 -0
- package/lib/harness/console-commands.d.ts +463 -0
- package/lib/harness/console-commands.js +3216 -0
- package/lib/harness/download-watch.d.ts +21 -0
- package/lib/harness/download-watch.js +156 -0
- package/lib/harness/extension-backend.d.ts +41 -0
- package/lib/harness/extension-backend.js +100 -0
- package/lib/harness/extension-host.d.ts +112 -0
- package/lib/harness/extension-host.js +552 -0
- package/lib/harness/extension-launch.d.ts +67 -0
- package/lib/harness/extension-launch.js +285 -0
- package/lib/harness/extension-link.d.ts +315 -0
- package/lib/harness/extension-link.js +607 -0
- package/lib/harness/external-manager.d.ts +146 -0
- package/lib/harness/external-manager.js +414 -0
- package/lib/harness/fixture.d.ts +165 -0
- package/lib/harness/fixture.js +461 -0
- package/lib/harness/frame-sessions.d.ts +71 -0
- package/lib/harness/frame-sessions.js +248 -0
- package/lib/harness/frames.d.ts +68 -0
- package/lib/harness/frames.js +216 -0
- package/lib/harness/help-format.d.ts +17 -0
- package/lib/harness/help-format.js +127 -0
- package/lib/harness/host.d.ts +352 -0
- package/lib/harness/host.js +423 -0
- package/lib/harness/index.d.ts +8 -0
- package/lib/harness/index.js +10 -0
- package/lib/harness/input-file.d.ts +18 -0
- package/lib/harness/input-file.js +61 -0
- package/lib/harness/json-output.d.ts +565 -0
- package/lib/harness/json-output.js +71 -0
- package/lib/harness/manager-actions.d.ts +21 -0
- package/lib/harness/manager-actions.js +129 -0
- package/lib/harness/manager-commands.d.ts +142 -0
- package/lib/harness/manager-commands.js +1196 -0
- package/lib/harness/manager-fetch.d.ts +33 -0
- package/lib/harness/manager-fetch.js +239 -0
- package/lib/harness/managers.d.ts +24 -0
- package/lib/harness/managers.js +27 -0
- package/lib/harness/output-style.d.ts +125 -0
- package/lib/harness/output-style.js +322 -0
- package/lib/harness/page-commands.d.ts +134 -0
- package/lib/harness/page-commands.js +1141 -0
- package/lib/harness/page-output.d.ts +67 -0
- package/lib/harness/page-output.js +139 -0
- package/lib/harness/page-watch.d.ts +171 -0
- package/lib/harness/page-watch.js +607 -0
- package/lib/harness/parse-check-node.d.ts +3 -0
- package/lib/harness/parse-check-node.js +132 -0
- package/lib/harness/path-args.d.ts +22 -0
- package/lib/harness/path-args.js +148 -0
- package/lib/harness/playwright-chromium.d.ts +16 -0
- package/lib/harness/playwright-chromium.js +86 -0
- package/lib/harness/prepare.d.ts +168 -0
- package/lib/harness/prepare.js +364 -0
- package/lib/harness/prompt-editor.d.ts +17 -0
- package/lib/harness/prompt-editor.js +354 -0
- package/lib/harness/prompt-history.d.ts +13 -0
- package/lib/harness/prompt-history.js +89 -0
- package/lib/harness/rebuild-advice.d.ts +6 -0
- package/lib/harness/rebuild-advice.js +39 -0
- package/lib/harness/run-log.d.ts +47 -0
- package/lib/harness/run-log.js +140 -0
- package/lib/harness/run-logs.d.ts +132 -0
- package/lib/harness/run-logs.js +241 -0
- package/lib/harness/run.d.ts +194 -0
- package/lib/harness/run.js +1973 -0
- package/lib/harness/runs.d.ts +229 -0
- package/lib/harness/runs.js +726 -0
- package/lib/harness/save-html.d.ts +39 -0
- package/lib/harness/save-html.js +89 -0
- package/lib/harness/saved-pages.d.ts +120 -0
- package/lib/harness/saved-pages.js +275 -0
- package/lib/harness/script-mark.d.ts +17 -0
- package/lib/harness/script-mark.js +56 -0
- package/lib/harness/snapshot.d.ts +143 -0
- package/lib/harness/snapshot.js +474 -0
- package/lib/harness/stuck-tabs.d.ts +19 -0
- package/lib/harness/stuck-tabs.js +131 -0
- package/lib/harness/tab-refs.d.ts +20 -0
- package/lib/harness/tab-refs.js +80 -0
- package/lib/harness/tampermonkey.d.ts +71 -0
- package/lib/harness/tampermonkey.js +356 -0
- package/lib/harness/tm-ids.d.ts +18 -0
- package/lib/harness/tm-ids.js +88 -0
- package/lib/harness/violentmonkey.d.ts +34 -0
- package/lib/harness/violentmonkey.js +162 -0
- package/lib/harness/wait.d.ts +77 -0
- package/lib/harness/wait.js +458 -0
- package/lib/http-read.d.ts +30 -0
- package/lib/http-read.js +142 -0
- package/lib/local-file-overrides.d.ts +35 -0
- package/lib/local-file-overrides.js +558 -0
- package/lib/manifest.json +38 -0
- package/lib/markup-response.d.ts +11 -0
- package/lib/markup-response.js +126 -0
- package/lib/match-targets.d.ts +18 -0
- package/lib/match-targets.js +305 -0
- package/lib/menu-captions.d.ts +1 -0
- package/lib/menu-captions.js +22 -0
- package/lib/parse-check-protocol.d.ts +27 -0
- package/lib/parse-check-protocol.js +51 -0
- package/lib/parse-location.d.ts +12 -0
- package/lib/parse-location.js +171 -0
- package/lib/phase-timing.d.ts +12 -0
- package/lib/phase-timing.js +155 -0
- package/lib/public-suffixes.d.ts +1 -0
- package/lib/public-suffixes.js +6957 -0
- package/lib/run-context.d.ts +2 -0
- package/lib/run-context.js +31 -0
- package/lib/runtime-messages.d.ts +10 -0
- package/lib/runtime-messages.js +34 -0
- package/lib/sandboxed-page.d.ts +5 -0
- package/lib/sandboxed-page.js +35 -0
- package/lib/script-build.d.ts +111 -0
- package/lib/script-build.js +787 -0
- package/lib/script-display.d.ts +51 -0
- package/lib/script-display.js +383 -0
- package/lib/script-icon.d.ts +16 -0
- package/lib/script-icon.js +43 -0
- package/lib/script-links.d.ts +24 -0
- package/lib/script-links.js +204 -0
- package/lib/script-source-url.d.ts +8 -0
- package/lib/script-source-url.js +214 -0
- package/lib/script-world.d.ts +6 -0
- package/lib/script-world.js +65 -0
- package/lib/sm-dev-protocol.d.ts +251 -0
- package/lib/sm-dev-protocol.js +84 -0
- package/lib/source-map.d.ts +32 -0
- package/lib/source-map.js +177 -0
- package/lib/stack-link.d.ts +15 -0
- package/lib/stack-link.js +138 -0
- package/lib/unsafe-window-use.d.ts +1 -0
- package/lib/unsafe-window-use.js +229 -0
- package/lib/url-match.d.ts +40 -0
- package/lib/url-match.js +734 -0
- package/lib/userscript-header.d.ts +33 -0
- package/lib/userscript-header.js +248 -0
- package/lib/version-compare.d.ts +6 -0
- package/lib/version-compare.js +141 -0
- package/lib/version-probe.d.ts +17 -0
- package/lib/version-probe.js +120 -0
- package/package.json +55 -3
- package/skills/SourceMonkey-dev/SKILL.md +868 -0
- package/skills/SourceMonkey-dev/references/cdp.md +293 -0
- package/skills/SourceMonkey-dev/references/commands.md +297 -0
- package/skills/SourceMonkey-dev/references/other-managers.md +63 -0
- package/skills/SourceMonkey-dev/references/specs.md +46 -0
- package/skills/SourceMonkey-friction/SKILL.md +103 -0
- package/skills/SourceMonkey-friction/references/template.md +59 -0
|
@@ -0,0 +1,991 @@
|
|
|
1
|
+
// Single entry point for every userscript refresh path.
|
|
2
|
+
//
|
|
3
|
+
// Two trigger modes share the same underlying work (refreshScript /
|
|
4
|
+
// refreshCollection / refreshAllCollections in script-store.ts):
|
|
5
|
+
//
|
|
6
|
+
// - 'manual' — the user clicked a Refresh button. Always runs.
|
|
7
|
+
// Awaited by the caller so the UI can show a
|
|
8
|
+
// spinner and react to the result.
|
|
9
|
+
// - 'opportunistic' — the SW noticed a page that uses some scripts
|
|
10
|
+
// and wants to check whether their source files
|
|
11
|
+
// changed on disk. Fire-and-forget. Rate-limited
|
|
12
|
+
// to no sooner than UPDATE_MIN_INTERVAL_MS after
|
|
13
|
+
// the previous completion, and never concurrent
|
|
14
|
+
// for the same resource.
|
|
15
|
+
//
|
|
16
|
+
// In both modes a check that's currently in flight for a resource is
|
|
17
|
+
// shared — a manual click that lands while an opportunistic check is
|
|
18
|
+
// already running awaits that existing promise instead of starting a
|
|
19
|
+
// second pass. This holds PER gate key: the script gate
|
|
20
|
+
// (`updateScript`), the collection gate (`updateCollection`), and the
|
|
21
|
+
// all-collections gate (`updateAllCollections`) are independent maps,
|
|
22
|
+
// so an opportunistic `updateScript(X)` can still run alongside a
|
|
23
|
+
// manual `updateCollection(S⊇X)` or `updateAllCollections` that also
|
|
24
|
+
// rebuilds X. They fetch the same bytes, and their storage writes are
|
|
25
|
+
// serialized and re-read user-owned state by `script-store.ts`'s
|
|
26
|
+
// `mutate()` lock (see docs/storage.md "Write serialization"), so the
|
|
27
|
+
// overlap is idempotent rather than corrupting — but it is NOT deduped
|
|
28
|
+
// to a single fetch.
|
|
29
|
+
//
|
|
30
|
+
// We deliberately don't try to mark tabs stale from here. The actual
|
|
31
|
+
// stale propagation happens via chrome.storage.onChanged in
|
|
32
|
+
// background.ts so that ANY write that changes a script's content
|
|
33
|
+
// (this module, external tools poking storage, future paths we haven't
|
|
34
|
+
// imagined) triggers the same propagation.
|
|
35
|
+
import { getAllScriptMetas, getScriptMeta, getCollections, getCollectionWithScriptMeta, loadStateLogEntry, localFileLogMessages, startupLogEntry, refreshAllCollections, refreshScript, refreshCollection, createRefreshBatch, setVersionProbes, } from './script-store.js';
|
|
36
|
+
import { outdatedProbe, probeLabel, probeTargets, probesAreFresh, } from '../version-probe.js';
|
|
37
|
+
import { isContainerCollection } from './directories.js';
|
|
38
|
+
import { probeVersions } from './version-probe-fetch.js';
|
|
39
|
+
import { mapWithConcurrency, PROBE_SCRIPTS_CONCURRENCY } from './concurrency.js';
|
|
40
|
+
import { describeRefreshError, readRefreshErrors } from './refresh-error.js';
|
|
41
|
+
import { getValue } from '../userscript-header.js';
|
|
42
|
+
import { formatBytes, scriptOutdatedProbe } from '../script-display.js';
|
|
43
|
+
import { dlog } from '../debug-log.js';
|
|
44
|
+
import { startPhaseTimer, resetPhaseTotals, reportPhaseTotals } from '../phase-timing.js';
|
|
45
|
+
import { githubTokenLogSuffix } from '../github-auth.js';
|
|
46
|
+
import { getScriptLogLabel } from './script-log-id.js';
|
|
47
|
+
import { appendScriptLog, logScriptEvent } from './script-log.js';
|
|
48
|
+
import { describeDiagnostic } from '../diagnostic-location.js';
|
|
49
|
+
import { retryRejectedRegistrations } from './injector.js';
|
|
50
|
+
// Minimum gap between opportunistic checks for the same resource. The
|
|
51
|
+
// 10s figure is a balance: short enough that a user editing a script
|
|
52
|
+
// and switching tabs sees the update within a click or two, long
|
|
53
|
+
// enough that the background work doesn't measurably affect navigation.
|
|
54
|
+
export const UPDATE_MIN_INTERVAL_MS = 10_000;
|
|
55
|
+
// Allow tests to swap the clock. Production uses Date.now.
|
|
56
|
+
let nowMsImpl = () => Date.now();
|
|
57
|
+
export function _setNowForTests(fn) {
|
|
58
|
+
nowMsImpl = fn;
|
|
59
|
+
}
|
|
60
|
+
// Pure-ish core: run `task` if and only if there isn't one already in
|
|
61
|
+
// flight for this resource, and (for opportunistic mode) the cooldown
|
|
62
|
+
// has elapsed. Returns the active promise either way. The state Map is
|
|
63
|
+
// mutated in place; callers pass in a stable per-resource-type map so
|
|
64
|
+
// independent resources can run concurrently.
|
|
65
|
+
//
|
|
66
|
+
// Exported so tests can exercise the concurrency/rate-limit logic
|
|
67
|
+
// without touching chrome.storage.
|
|
68
|
+
export function gate(state, key, mode, task, skipResult,
|
|
69
|
+
// How the rate-limit skip below names this resource. Only the caller
|
|
70
|
+
// knows whether the key is a script, a collection, or the
|
|
71
|
+
// all-collections sentinel, and only a script has a readable label —
|
|
72
|
+
// so the naming is passed in rather than guessed from the key.
|
|
73
|
+
label = String(key),
|
|
74
|
+
// A caller that needs its own run's result (a Refresh click: forced
|
|
75
|
+
// probe, awaited parse check) rather than a running check's. It waits
|
|
76
|
+
// for that check, then runs, and later callers join it instead.
|
|
77
|
+
own = false) {
|
|
78
|
+
let slot = state.get(key);
|
|
79
|
+
if (!slot) {
|
|
80
|
+
slot = {};
|
|
81
|
+
state.set(key, slot);
|
|
82
|
+
}
|
|
83
|
+
// Coalesce onto any in-flight work for this resource regardless of
|
|
84
|
+
// mode, unless the caller needs its own run (`own`). A manual check
|
|
85
|
+
// during a background one shouldn't start a second fetch; it should
|
|
86
|
+
// wait for the running one.
|
|
87
|
+
const running = slot.inFlight?.promise;
|
|
88
|
+
if (running && !own)
|
|
89
|
+
return running;
|
|
90
|
+
if (mode === 'opportunistic') {
|
|
91
|
+
const last = slot.lastCompletedAt ?? 0;
|
|
92
|
+
const since = nowMsImpl() - last;
|
|
93
|
+
if (since < UPDATE_MIN_INTERVAL_MS) {
|
|
94
|
+
// Logged because a rate-limited check does nothing else: no
|
|
95
|
+
// fetch, no storage write, no other log line. Without this it
|
|
96
|
+
// looks identical to a check that never started.
|
|
97
|
+
dlog(`[SourceMonkey] update check skipped for ${label}: ` +
|
|
98
|
+
`checked ${Math.round(since / 1000)}s ago, interval is ${UPDATE_MIN_INTERVAL_MS / 1000}s`);
|
|
99
|
+
return Promise.resolve(skipResult);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// The slot entry first: its cleanup compares the slot against it.
|
|
103
|
+
const entry = {};
|
|
104
|
+
entry.promise = (async () => {
|
|
105
|
+
if (running)
|
|
106
|
+
await running.catch(() => undefined);
|
|
107
|
+
try {
|
|
108
|
+
return await task();
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
const s = state.get(key);
|
|
112
|
+
if (s) {
|
|
113
|
+
// Only its own entry: an `own` run may have taken the slot since.
|
|
114
|
+
if (s.inFlight === entry)
|
|
115
|
+
s.inFlight = undefined;
|
|
116
|
+
s.lastCompletedAt = nowMsImpl();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
})();
|
|
120
|
+
slot.inFlight = entry;
|
|
121
|
+
return entry.promise;
|
|
122
|
+
}
|
|
123
|
+
// Separate maps per result type so type inference stays clean.
|
|
124
|
+
const scriptState = new Map();
|
|
125
|
+
const collectionState = new Map();
|
|
126
|
+
const allCollectionsState = new Map();
|
|
127
|
+
// "all collections" is one global slot, but reuse the same shape so the
|
|
128
|
+
// concurrency/rate-limit logic stays uniform.
|
|
129
|
+
const ALL_COLLECTIONS_KEY = '*';
|
|
130
|
+
// Public API ---------------------------------------------------------
|
|
131
|
+
// `probe` is separate from `mode` on purpose. `manual` means "not
|
|
132
|
+
// rate-limited as a refresh", and the reload fast-path uses it for
|
|
133
|
+
// every matched script on every explicit tab reload — so treating
|
|
134
|
+
// `manual` as "the user asked, check upstream now" would fire a
|
|
135
|
+
// third-party request per script per reload. Only a genuine Refresh
|
|
136
|
+
// click passes `'force'`.
|
|
137
|
+
//
|
|
138
|
+
// `parseCheck` follows one rule, and it is deliberately not the same
|
|
139
|
+
// axis as `mode`: **a navigation-triggered check never waits for a
|
|
140
|
+
// parse verdict; a user-initiated one does.** Both of this module's
|
|
141
|
+
// navigation callers therefore pass 'defer' — the reload fast path,
|
|
142
|
+
// which has a wrapper in the page blocked on the answer, and the
|
|
143
|
+
// opportunistic sweep, which has nobody waiting at all.
|
|
144
|
+
//
|
|
145
|
+
// The rule has to be uniform because `gate()` coalesces regardless of
|
|
146
|
+
// mode, and both of those callers are queued off the same
|
|
147
|
+
// `onCommitted`. If one of them waited for the verdict, whichever
|
|
148
|
+
// reached the gate first would decide for both — so the fast path
|
|
149
|
+
// would silently pay the check on exactly the first reload after an
|
|
150
|
+
// idle gap, which is the ordinary edit-save-reload loop.
|
|
151
|
+
//
|
|
152
|
+
// The same coalescing would let a user-initiated check inherit a
|
|
153
|
+
// verdict-less row, and miss its forced probe. So a check that forces
|
|
154
|
+
// the probe (a Refresh click, from a page or from sm-dev) doesn't join a
|
|
155
|
+
// running one: it waits for it, then runs its own (`own` in `gate`).
|
|
156
|
+
// `batch` shares the reads a group of checks started together would
|
|
157
|
+
// otherwise each do (see `RefreshBatch`). Every navigation-triggered
|
|
158
|
+
// fan-out passes one — the reload fast path and the opportunistic
|
|
159
|
+
// sweep share the batch their common `onCommitted` created. The
|
|
160
|
+
// user-initiated single-script callers (the Refresh buttons) don't:
|
|
161
|
+
// they check one script, so there is nothing to share.
|
|
162
|
+
//
|
|
163
|
+
// **A coalesced call inherits the running check's batch, and that is
|
|
164
|
+
// safe.** `gate()` hands a second caller the FIRST call's promise, so it
|
|
165
|
+
// has always received that call's whole result — rows, verdicts and all.
|
|
166
|
+
// Which cache produced those rows changes nothing: the coalescing caller
|
|
167
|
+
// never had a refresh of its own to be given stale data from.
|
|
168
|
+
export function updateScript(scriptId, mode, probe = 'if-stale', parseCheck = 'await', batch) {
|
|
169
|
+
return gate(scriptState, scriptId, mode, () => runScriptCheck(scriptId, mode, probe, parseCheck, batch), null, getScriptLogLabel(scriptId), probe === 'force');
|
|
170
|
+
}
|
|
171
|
+
// Only ever reached from a Refresh button, so probes force by default.
|
|
172
|
+
export function updateCollection(collectionId, mode, probe = 'force') {
|
|
173
|
+
return gate(collectionState, collectionId, mode, () => runCollectionCheck(collectionId, mode, probe), null, `collection ${collectionId}`);
|
|
174
|
+
}
|
|
175
|
+
// Only ever reached from Refresh All, so probes force by default.
|
|
176
|
+
export function updateAllCollections(mode, probe = 'force') {
|
|
177
|
+
// We have to do the cast dance because gate is generic over a single
|
|
178
|
+
// result type and the skip result for "no work done" is just an empty
|
|
179
|
+
// map, not null.
|
|
180
|
+
return gate(allCollectionsState, ALL_COLLECTIONS_KEY, mode, () => runAllCollectionsCheck(mode, probe), new Map(), 'all collections');
|
|
181
|
+
}
|
|
182
|
+
// ---------- Task wrappers that log lifecycle to the SW console -------
|
|
183
|
+
//
|
|
184
|
+
// Each user-visible check goes through these so the SW console tells
|
|
185
|
+
// the story: "checking X", then either "no change", "loaded new
|
|
186
|
+
// content", or "failed: <err>". Without this it's hard to tell
|
|
187
|
+
// whether the opportunistic refresh is even running.
|
|
188
|
+
//
|
|
189
|
+
// We do a getScript / getCollectionWithScripts read BEFORE the refresh
|
|
190
|
+
// so we have the prior loadedAt to compare against the post-refresh
|
|
191
|
+
// row. One extra storage read per check — cheap.
|
|
192
|
+
// `@require` + `@resource` policy by trigger mode. Manual clicks
|
|
193
|
+
// rebuild deps from scratch so users can pick up remote updates
|
|
194
|
+
// without touching the main script. Opportunistic background checks
|
|
195
|
+
// preserve the cached entries verbatim and only fetch newly-declared
|
|
196
|
+
// refs — keeping page-load cost bounded.
|
|
197
|
+
function fileRecheckMode(mode) {
|
|
198
|
+
return mode === 'manual' ? 'all' : 'preserve';
|
|
199
|
+
}
|
|
200
|
+
async function runScriptCheck(scriptId, mode, probe, parseCheck = 'await', batch) {
|
|
201
|
+
const prior = await getScriptMeta(scriptId);
|
|
202
|
+
const label = getScriptLogLabel(scriptId);
|
|
203
|
+
// Manual checks fire on a deliberate user click and are infrequent;
|
|
204
|
+
// their start + "no change" outcome stays at console.log so the user
|
|
205
|
+
// gets unconditional confirmation that the click did something.
|
|
206
|
+
// Opportunistic checks fire on every navigation and are gated by the
|
|
207
|
+
// debug flag.
|
|
208
|
+
const checkLog = mode === 'manual' ? console.log : dlog;
|
|
209
|
+
const tokenSuffix = prior?.fileUrl ? githubTokenLogSuffix(prior.fileUrl) : '';
|
|
210
|
+
checkLog(`[SourceMonkey] update check (${mode}): ${label}${tokenSuffix}`);
|
|
211
|
+
// No Log-tab row before the work: `emitScriptOutcomeLog` writes the
|
|
212
|
+
// whole story afterwards, as one line when there is nothing to report
|
|
213
|
+
// and a `Checking for updates` preamble when there is. The SW console
|
|
214
|
+
// above still brackets the work for anyone watching it live.
|
|
215
|
+
let next;
|
|
216
|
+
try {
|
|
217
|
+
next = await refreshScript(scriptId, {
|
|
218
|
+
recheckFiles: fileRecheckMode(mode),
|
|
219
|
+
parseCheck,
|
|
220
|
+
...(batch ? { batch } : {}),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
const msg = describe(err);
|
|
225
|
+
console.info(`[SourceMonkey] update check failed: ${label}: ${msg}`);
|
|
226
|
+
// Stands alone — it names the check and its failure in one line,
|
|
227
|
+
// so it needs no preamble.
|
|
228
|
+
//
|
|
229
|
+
// `state: 'refresh-failed'` even though the row carries no
|
|
230
|
+
// `refreshError` (the throw came from before the row was written):
|
|
231
|
+
// the meaning is the same — this check didn't complete and the
|
|
232
|
+
// cached copy keeps running — and a bare `error` would bump the
|
|
233
|
+
// `loglevel:` sentinel and leave the script badged ERROR for the
|
|
234
|
+
// rest of the session over a failure the next check may not repeat.
|
|
235
|
+
logScriptEvent(scriptId, {
|
|
236
|
+
level: 'error',
|
|
237
|
+
state: 'refresh-failed',
|
|
238
|
+
source: refreshLogSource(mode),
|
|
239
|
+
message: `Could not check for updates: ${msg}`,
|
|
240
|
+
});
|
|
241
|
+
throw err;
|
|
242
|
+
}
|
|
243
|
+
logScriptOutcome(label, prior, next, mode);
|
|
244
|
+
emitScriptOutcomeLog(scriptId, prior, next, mode);
|
|
245
|
+
// Deliberately NOT awaited. This promise is what the reload
|
|
246
|
+
// fast-path races against a 500 ms window (see
|
|
247
|
+
// docs/injection.md#reload-fast-path) — folding a third-party fetch
|
|
248
|
+
// into it would blow that budget and drop the reload back to running
|
|
249
|
+
// the old body. The probe's own storage write repaints any open
|
|
250
|
+
// viewer via storage.onChanged when it lands.
|
|
251
|
+
if (next)
|
|
252
|
+
void runVersionProbes([next], probe);
|
|
253
|
+
// Not awaited, same as the probe above: it must not sit in the
|
|
254
|
+
// fast-path window. No-ops unless this script is currently rejected,
|
|
255
|
+
// in which case a refresh that changed nothing would otherwise never
|
|
256
|
+
// re-offer it to Chrome. See `retryRejectedRegistrations`.
|
|
257
|
+
//
|
|
258
|
+
// `probe === 'force'` is the only signal we have for "the user
|
|
259
|
+
// clicked Refresh" — `mode: 'manual'` means "not rate-limited" and
|
|
260
|
+
// the reload fast-path uses it for every matched script on every
|
|
261
|
+
// reload. A click retries immediately; a navigation-driven check
|
|
262
|
+
// takes the rate limit, so one page load can't produce a burst of
|
|
263
|
+
// identical rows.
|
|
264
|
+
void retryRejectedRegistrations([scriptId], { userInitiated: probe === 'force' });
|
|
265
|
+
return next;
|
|
266
|
+
}
|
|
267
|
+
async function runCollectionCheck(collectionId, mode, probe) {
|
|
268
|
+
const priorData = await getCollectionWithScriptMeta(collectionId);
|
|
269
|
+
const prior = priorData
|
|
270
|
+
? { collection: priorData.collection, scripts: new Map(priorData.scripts.map((s) => [s.id, s])) }
|
|
271
|
+
: undefined;
|
|
272
|
+
const label = priorData?.collection.path ?? collectionId;
|
|
273
|
+
// Suffix the start line with "(with GitHub token)" when the
|
|
274
|
+
// collection URL would carry the GitHub auth header on fetch. Lets
|
|
275
|
+
// the user confirm from the SW console that their Options-page token
|
|
276
|
+
// is actually in play for this collection (otherwise a silent "still
|
|
277
|
+
// hitting unauthenticated rate limits" is hard to diagnose).
|
|
278
|
+
const tokenSuffix = prior?.collection.path ? githubTokenLogSuffix(prior.collection.path) : '';
|
|
279
|
+
console.log(`[SourceMonkey] collection refresh (${mode}): ${label}${tokenSuffix}`);
|
|
280
|
+
// Sits OUTSIDE refreshCollection's own timer rather than extending it:
|
|
281
|
+
// the probes and the rejection retry are this layer's work, and the
|
|
282
|
+
// question "is the refresh slow or is the annotation pass slow?" is
|
|
283
|
+
// exactly what the split answers.
|
|
284
|
+
const timer = startPhaseTimer(`collection check (${mode})`);
|
|
285
|
+
resetPhaseTotals();
|
|
286
|
+
let result;
|
|
287
|
+
try {
|
|
288
|
+
result = await refreshCollection(collectionId, { recheckFiles: fileRecheckMode(mode) });
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
const msg = describe(err);
|
|
292
|
+
console.info(`[SourceMonkey] collection refresh failed: ${label}: ${msg}`);
|
|
293
|
+
// A throw out of refreshCollection leaves every script under this
|
|
294
|
+
// collection unrefreshed. Say so in each one's Log rather than
|
|
295
|
+
// letting the failure live only in the SW console.
|
|
296
|
+
logCollectionEventToScripts([...(prior?.scripts.values() ?? [])], mode, await collectionFailureMessage(collectionId, msg));
|
|
297
|
+
throw err;
|
|
298
|
+
}
|
|
299
|
+
timer.mark('refresh');
|
|
300
|
+
if (result) {
|
|
301
|
+
summarizeCollectionOutcome(label, prior, result, mode);
|
|
302
|
+
await runVersionProbes(result.scripts, probe);
|
|
303
|
+
timer.mark('probes');
|
|
304
|
+
timer.note(() => `${probedCount(result.scripts)} of ${result.scripts.length} scripts have a probe target`);
|
|
305
|
+
// Retry any script under this collection that Chrome is refusing —
|
|
306
|
+
// refreshing a collection is one of the places a user goes to say
|
|
307
|
+
// "try again". See `retryRejectedRegistrations`.
|
|
308
|
+
await retryRejectedRegistrations(result.scripts.map((s) => s.id), { userInitiated: probe === 'force' });
|
|
309
|
+
timer.mark('retry-rejected');
|
|
310
|
+
}
|
|
311
|
+
timer.done();
|
|
312
|
+
reportPhaseTotals('collection check');
|
|
313
|
+
return result;
|
|
314
|
+
}
|
|
315
|
+
// How many of these scripts a probe pass would actually fetch for.
|
|
316
|
+
// Reported alongside the probe phase because a `0ms` probe leg means
|
|
317
|
+
// two very different things — nothing to check, or everything already
|
|
318
|
+
// fresh — and only the count separates them.
|
|
319
|
+
function probedCount(scripts) {
|
|
320
|
+
return scripts.filter((s) => s.status === 'ok' && probeTargets(s.header, s.fileUrl).length > 0).length;
|
|
321
|
+
}
|
|
322
|
+
// The one sentence a collection-level refresh failure gets in every
|
|
323
|
+
// affected script's Log, whichever entry point caught it. Both callers
|
|
324
|
+
// route through here so the same failure can't read two ways.
|
|
325
|
+
//
|
|
326
|
+
// Which sentence depends on WHERE it failed, and the recorded map is the
|
|
327
|
+
// evidence: `refreshCollection` records only when its write throws, so
|
|
328
|
+
// an entry means the collection was read fine and couldn't be stored.
|
|
329
|
+
// A throw with nothing recorded came from earlier in the pipeline — a
|
|
330
|
+
// storage read, a hash, a rebuild — and must not claim the read
|
|
331
|
+
// succeeded, so it gets the generic wording.
|
|
332
|
+
async function collectionFailureMessage(collectionId, fallbackReason) {
|
|
333
|
+
const recorded = (await readRefreshErrors())[collectionId];
|
|
334
|
+
return recorded
|
|
335
|
+
? describeRefreshError(recorded)
|
|
336
|
+
: `Could not refresh this collection: ${fallbackReason}`;
|
|
337
|
+
}
|
|
338
|
+
async function runAllCollectionsCheck(mode, probe) {
|
|
339
|
+
console.log(`[SourceMonkey] refreshing all collections (${mode})`);
|
|
340
|
+
// Same split as runCollectionCheck: each collection times its own
|
|
341
|
+
// refresh, so what this adds is the work Refresh All does AROUND
|
|
342
|
+
// them — the prior snapshot, and the probe + retry pass that runs
|
|
343
|
+
// once over every script in every collection rather than per
|
|
344
|
+
// collection.
|
|
345
|
+
const timer = startPhaseTimer(`all-collections check (${mode})`);
|
|
346
|
+
resetPhaseTotals();
|
|
347
|
+
// Snapshot priors BEFORE the refresh; reading after would just give
|
|
348
|
+
// back the post-refresh state and the summary would always say 0
|
|
349
|
+
// updated / 0 added.
|
|
350
|
+
const priorByCollectionId = await snapshotAllCollectionScripts();
|
|
351
|
+
timer.mark('snapshot');
|
|
352
|
+
let result;
|
|
353
|
+
try {
|
|
354
|
+
result = await refreshAllCollections({ recheckFiles: fileRecheckMode(mode) });
|
|
355
|
+
}
|
|
356
|
+
catch (err) {
|
|
357
|
+
const msg = describe(err);
|
|
358
|
+
console.info(`[SourceMonkey] refresh all failed: ${msg}`);
|
|
359
|
+
// Reached only when the PASS itself broke — a per-collection failure
|
|
360
|
+
// is recorded and skipped inside `refreshAllCollections`, and handled
|
|
361
|
+
// below. Nothing was summarized here, so every script hears the same
|
|
362
|
+
// reason. Same fan-out as runCollectionCheck so the two paths can't
|
|
363
|
+
// drift.
|
|
364
|
+
for (const snapshot of priorByCollectionId.values()) {
|
|
365
|
+
logCollectionEventToScripts([...snapshot.scripts.values()], mode, `Could not refresh this collection: ${msg}`);
|
|
366
|
+
}
|
|
367
|
+
throw err;
|
|
368
|
+
}
|
|
369
|
+
// Collections that were refreshed but couldn't be saved are absent
|
|
370
|
+
// from `result` without the pass having failed. They still owe their
|
|
371
|
+
// scripts a Log entry — a Refresh All that silently skipped this would
|
|
372
|
+
// say less than refreshing the same collection on its own, which does
|
|
373
|
+
// log (see runCollectionCheck).
|
|
374
|
+
//
|
|
375
|
+
// Keyed off the recorded map so each collection gets ITS reason, and so
|
|
376
|
+
// a collection absent for the other reason — removed mid-refresh, which
|
|
377
|
+
// clears its entry — stays quiet.
|
|
378
|
+
timer.mark('refresh');
|
|
379
|
+
const refreshErrors = await readRefreshErrors();
|
|
380
|
+
for (const [id, snapshot] of priorByCollectionId) {
|
|
381
|
+
if (result.has(id))
|
|
382
|
+
continue;
|
|
383
|
+
const reason = refreshErrors[id];
|
|
384
|
+
if (!reason)
|
|
385
|
+
continue;
|
|
386
|
+
logCollectionEventToScripts([...snapshot.scripts.values()], mode, describeRefreshError(reason));
|
|
387
|
+
}
|
|
388
|
+
for (const [, refreshed] of result) {
|
|
389
|
+
summarizeCollectionOutcome(refreshed.collection.path, priorByCollectionId.get(refreshed.collection.id), refreshed, mode);
|
|
390
|
+
}
|
|
391
|
+
const allScripts = [...result.values()].flatMap((r) => r.scripts);
|
|
392
|
+
timer.mark('summarize');
|
|
393
|
+
await runVersionProbes(allScripts, probe);
|
|
394
|
+
timer.mark('probes');
|
|
395
|
+
// Counted, not just timed: each probe result is its own storage write,
|
|
396
|
+
// and a write is what schedules a registration re-sync. The number of
|
|
397
|
+
// probed scripts is therefore the number of re-syncs this pass can
|
|
398
|
+
// provoke, which is not something the elapsed time shows.
|
|
399
|
+
timer.note(() => `${probedCount(allScripts)} of ${allScripts.length} scripts have a probe target`);
|
|
400
|
+
// Refresh All is the other "try again" button, so it retries every
|
|
401
|
+
// refused registration it just re-read. See
|
|
402
|
+
// `retryRejectedRegistrations`.
|
|
403
|
+
await retryRejectedRegistrations(allScripts.map((s) => s.id), { userInitiated: probe === 'force' });
|
|
404
|
+
timer.mark('retry-rejected');
|
|
405
|
+
timer.done();
|
|
406
|
+
reportPhaseTotals('all-collections check');
|
|
407
|
+
return result;
|
|
408
|
+
}
|
|
409
|
+
// ---------- Update probes ----------
|
|
410
|
+
//
|
|
411
|
+
// Runs after the refresh, against the rows it produced. Deliberately
|
|
412
|
+
// downstream of everything above: a probe can't change a status, can't
|
|
413
|
+
// set a refreshError, and a probe host that's down or slow must leave
|
|
414
|
+
// the refresh's own outcome exactly as it was. See src/version-probe.ts
|
|
415
|
+
// for what "probe" means here and [docs/updates.md] for why we don't
|
|
416
|
+
// simply update from the probed URL.
|
|
417
|
+
//
|
|
418
|
+
// Awaited by the collection-level entry points (a Refresh button click,
|
|
419
|
+
// where the user is watching a spinner and expects the row to be
|
|
420
|
+
// current when it stops) but not by the per-script one, which sits on
|
|
421
|
+
// the navigation hot path — see runScriptCheck.
|
|
422
|
+
async function runVersionProbes(scripts, probe) {
|
|
423
|
+
// Bounded for the same reason the refresh fan-out is (see
|
|
424
|
+
// src/extension/concurrency.ts): "Refresh All" hands this every script in every
|
|
425
|
+
// collection, and each one expands into its own pool of probe fetches.
|
|
426
|
+
// Unbounded here would put the product in flight at once.
|
|
427
|
+
await mapWithConcurrency(scripts, PROBE_SCRIPTS_CONCURRENCY, (s) => probeOneScript(s, probe).catch(() => {
|
|
428
|
+
// A probe is an annotation. Nothing downstream depends on it, so a
|
|
429
|
+
// failure that escaped runProbe's own handling dies here rather
|
|
430
|
+
// than propagating into the refresh the user actually asked for.
|
|
431
|
+
}));
|
|
432
|
+
}
|
|
433
|
+
// After a single-script install: check for a newer version at once, so
|
|
434
|
+
// an install of an already-outdated copy says so without waiting for a
|
|
435
|
+
// Refresh. Not awaited by the install, like the per-script refresh's:
|
|
436
|
+
// the result lands on the row, and the pages repaint from it. A whole
|
|
437
|
+
// collection added at once isn't probed here; its first Refresh does.
|
|
438
|
+
export function probeInstalledScript(result) {
|
|
439
|
+
const id = result?.kind === 'existingContainer' ? result.scriptId
|
|
440
|
+
: result && isContainerCollection(result.collection.path) ? result.collection.scriptIds[0]
|
|
441
|
+
: undefined;
|
|
442
|
+
if (!id)
|
|
443
|
+
return;
|
|
444
|
+
void (async () => {
|
|
445
|
+
const script = await getScriptMeta(id);
|
|
446
|
+
if (script)
|
|
447
|
+
await runVersionProbes([script], 'force');
|
|
448
|
+
})().catch(() => {
|
|
449
|
+
// An annotation, as in `runVersionProbes`: the install stands.
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
async function probeOneScript(script, policy) {
|
|
453
|
+
// Only rows that are actually running get probed. An `invalid` row's
|
|
454
|
+
// header may not have parsed at all. Its stored probes are left alone
|
|
455
|
+
// rather than cleared: INVALID outranks OUTDATED so nothing stale is
|
|
456
|
+
// on screen, and a row that recovers gets its annotation back without
|
|
457
|
+
// a refetch.
|
|
458
|
+
if (script.status !== 'ok')
|
|
459
|
+
return;
|
|
460
|
+
const loadedVersion = script.header ? getValue(script.header, 'version') : undefined;
|
|
461
|
+
const targets = probeTargets(script.header, script.fileUrl);
|
|
462
|
+
if (targets.length === 0) {
|
|
463
|
+
// The script no longer names an upstream we don't already fetch
|
|
464
|
+
// from (or never did). Clear any annotation left from when it did,
|
|
465
|
+
// so a removed `@updateURL` doesn't strand an OUTDATED badge.
|
|
466
|
+
if (script.versionProbes) {
|
|
467
|
+
const wasOutdated = outdatedProbe(loadedVersion, script.versionProbes);
|
|
468
|
+
await setVersionProbes(script.id, []);
|
|
469
|
+
if (wasOutdated)
|
|
470
|
+
logUpToDateAgain(script.id, loadedVersion);
|
|
471
|
+
}
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const now = nowMsImpl();
|
|
475
|
+
// Everything but an explicit Refresh click respects the interval —
|
|
476
|
+
// including the reload fast-path, which runs in `manual` mode on
|
|
477
|
+
// every tab reload.
|
|
478
|
+
if (policy === 'if-stale' && probesAreFresh(script.versionProbes, targets, now))
|
|
479
|
+
return;
|
|
480
|
+
const before = outdatedProbe(loadedVersion, script.versionProbes);
|
|
481
|
+
const probes = await probeVersions(targets, now, script.versionProbes);
|
|
482
|
+
await setVersionProbes(script.id, probes);
|
|
483
|
+
// Report the transition, not the state: an upstream that has been
|
|
484
|
+
// ahead of us for a month shouldn't add a Log entry every interval.
|
|
485
|
+
// Both directions carry the `state` they leave the badge in, so the
|
|
486
|
+
// Log's status column reads OUTDATED / OK exactly where the badge
|
|
487
|
+
// changed.
|
|
488
|
+
//
|
|
489
|
+
// `before` is computed against the row this probe was handed, which
|
|
490
|
+
// is the freshly refreshed one — so "our copy caught up" is already
|
|
491
|
+
// not outdated here. That transition is reported by
|
|
492
|
+
// `emitScriptOutcomeLog`, which has the prior row; this one only sees
|
|
493
|
+
// the upstream side moving (a version rolled back).
|
|
494
|
+
const after = outdatedProbe(loadedVersion, probes);
|
|
495
|
+
if (!after) {
|
|
496
|
+
if (before)
|
|
497
|
+
logUpToDateAgain(script.id, loadedVersion);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
if (after.version === before?.version)
|
|
501
|
+
return;
|
|
502
|
+
const entry = outdatedLogEntry(after, loadedVersion);
|
|
503
|
+
console.info(`[SourceMonkey] ${getScriptLogLabel(script.id)}: ${entry.message}`);
|
|
504
|
+
// Not split by trigger mode like the refresh labels: this entry
|
|
505
|
+
// reports a transition ("an upstream copy just moved ahead of us"),
|
|
506
|
+
// and whether a click or a navigation noticed it tells the user
|
|
507
|
+
// nothing they'd act on.
|
|
508
|
+
logScriptEvent(script.id, { ...entry, source: 'version-probe' });
|
|
509
|
+
}
|
|
510
|
+
// The OUTDATED row, minus its source: written by the probe when the
|
|
511
|
+
// upstream moves ahead, and restated at boot for a probe that persisted
|
|
512
|
+
// across a restart. One formatter so both read alike.
|
|
513
|
+
function outdatedLogEntry(probe, loadedVersion) {
|
|
514
|
+
return {
|
|
515
|
+
level: 'warning',
|
|
516
|
+
state: 'outdated',
|
|
517
|
+
message: `Newer version ${probe.version} at ${probeLabel(probe.kinds)} ` +
|
|
518
|
+
`(running ${loadedVersion}): ${probe.url}`,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
// The OUTDATED → OK row. One message for every way the badge can go:
|
|
522
|
+
// our copy caught up (seen by the refresh outcome), the upstream rolled
|
|
523
|
+
// back (seen by the probe), or the header stopped naming an upstream
|
|
524
|
+
// (the probe's clear path).
|
|
525
|
+
function logUpToDateAgain(scriptId, loadedVersion) {
|
|
526
|
+
logScriptEvent(scriptId, {
|
|
527
|
+
level: 'info', state: 'ok', source: 'version-probe',
|
|
528
|
+
message: `Up to date again (running ${loadedVersion ?? '<none>'})`,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
// Pre-refresh state per collection: the collection row (for its
|
|
532
|
+
// `scanError`, so a still-down collection doesn't re-log) plus its
|
|
533
|
+
// scripts by ID.
|
|
534
|
+
async function snapshotAllCollectionScripts() {
|
|
535
|
+
const collections = await getCollections();
|
|
536
|
+
const entries = await Promise.all(collections.map(async (s) => {
|
|
537
|
+
const data = await getCollectionWithScriptMeta(s.id);
|
|
538
|
+
const scripts = new Map((data?.scripts ?? []).map((sc) => [sc.id, sc]));
|
|
539
|
+
return [s.id, { collection: s, scripts }];
|
|
540
|
+
}));
|
|
541
|
+
return new Map(entries);
|
|
542
|
+
}
|
|
543
|
+
// ---------- Logging helpers ----------
|
|
544
|
+
function logScriptOutcome(label, prior, next, mode) {
|
|
545
|
+
if (!next) {
|
|
546
|
+
console.info(`[SourceMonkey] update check: ${label}: row vanished`);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
if (next.status === 'invalid') {
|
|
550
|
+
console.info(`[SourceMonkey] update check: ${label}: invalid: ` +
|
|
551
|
+
`${describeDiagnostic(next.errorMessage ?? 'unknown', next.errorLocation)}`);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
// Refresh failed but the cached good copy keeps running. Log once on
|
|
555
|
+
// the transition (or when the reason changes) so a collection that
|
|
556
|
+
// stays down doesn't spam the console on every opportunistic check.
|
|
557
|
+
if (next.refreshError && prior?.refreshError !== next.refreshError) {
|
|
558
|
+
console.info(`[SourceMonkey] refresh failed: ${label}: ${next.refreshError}`);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
if (!prior || prior.loadedAt !== next.loadedAt) {
|
|
562
|
+
// loadedAt bumps iff the load pipeline actually built from fresh
|
|
563
|
+
// content. Same loadedAt means the size+mtime skip caught it.
|
|
564
|
+
if (dependenciesOnlyChange(prior, next)) {
|
|
565
|
+
console.log(`[SourceMonkey] loaded new content for its @require or @resource files: ${label}`);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
const size = next.size != null ? ` (${formatBytes(next.size)})` : '';
|
|
569
|
+
console.log(`[SourceMonkey] loaded new content: ${label}${size}`);
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
// "no change" pairs with the entry log: visible on manual, gated
|
|
573
|
+
// on opportunistic. Otherwise a Refresh click could log "update
|
|
574
|
+
// check" with nothing following.
|
|
575
|
+
const outcomeLog = mode === 'manual' ? console.log : dlog;
|
|
576
|
+
outcomeLog(`[SourceMonkey] no change: ${label}`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
// A new `loadedAt` that came from a `@require` / `@resource` alone: the
|
|
580
|
+
// script's own content hash is the same (see `dependenciesChanged` in
|
|
581
|
+
// src/script-build.ts).
|
|
582
|
+
function dependenciesOnlyChange(prior, next) {
|
|
583
|
+
return prior?.contentHash !== undefined && prior.contentHash === next.contentHash;
|
|
584
|
+
}
|
|
585
|
+
// Write a check's whole story into the per-script Log, mirroring the
|
|
586
|
+
// SW-console outcome line. The uneventful outcome is manual-only, so
|
|
587
|
+
// background polling doesn't fill the log with rows saying a healthy
|
|
588
|
+
// script is still healthy — those still get debug-gated console output
|
|
589
|
+
// via dlog.
|
|
590
|
+
//
|
|
591
|
+
// Every failure the UI surfaces on a script row also lands here, so
|
|
592
|
+
// clicking a red/amber badge always leads to an explanation: the entry
|
|
593
|
+
// carries the `state` the badge shows (INVALID / REFRESH FAILED, and
|
|
594
|
+
// OK for the recovery) — see `loadStateLogEntry` for the
|
|
595
|
+
// phrasing.
|
|
596
|
+
//
|
|
597
|
+
// **One row per uneventful check, two when there's news.** A check
|
|
598
|
+
// that finds nothing writes a single `Checked for updates and found no change`
|
|
599
|
+
// line. A check with something to report writes `Checking for updates`
|
|
600
|
+
// and then the outcome, which carries its own severity and is what the
|
|
601
|
+
// status badge links to.
|
|
602
|
+
//
|
|
603
|
+
// "Still broken" is a freshly computed verdict, not an echo: an
|
|
604
|
+
// `invalid` row can't take the size+mtime skip (`canSkip` requires
|
|
605
|
+
// `status === 'ok'`), so every check refetches and re-runs split +
|
|
606
|
+
// parse + validate.
|
|
607
|
+
//
|
|
608
|
+
// **Opportunistic checks log too.** They report the same facts a manual
|
|
609
|
+
// check would, and the whole point of a background check is to notice a
|
|
610
|
+
// failure the user hasn't gone looking for yet — a badge that turns
|
|
611
|
+
// amber on its own should have a matching Log line. (Trigger mode still
|
|
612
|
+
// gates the *non*-events: the uneventful row is manual-only.)
|
|
613
|
+
//
|
|
614
|
+
// Exported for unit tests — the public entry points would need the
|
|
615
|
+
// whole chrome.storage pipeline stubbed to reach it.
|
|
616
|
+
export function emitScriptOutcomeLog(scriptId, prior, next, mode) {
|
|
617
|
+
const source = refreshLogSource(mode);
|
|
618
|
+
// A check that found nothing to say is ONE line naming both the
|
|
619
|
+
// check and its result — see `NO_CHANGE_MESSAGE`. Only a check with
|
|
620
|
+
// something to report gets a preamble, because there the result is
|
|
621
|
+
// its own line (and its own severity) and needs the check named
|
|
622
|
+
// separately.
|
|
623
|
+
//
|
|
624
|
+
// Written after the work, so both halves land together. The SW
|
|
625
|
+
// console is where a check is bracketed live; the Log tab is a
|
|
626
|
+
// record of what happened.
|
|
627
|
+
// A row that went away during the check — deleted by the user, or
|
|
628
|
+
// dropped by this very check because its file was no longer listed
|
|
629
|
+
// — has no Log any more: `removeScript` cleared it. Writing here
|
|
630
|
+
// would leave an orphan `log:` row for a script nothing displays.
|
|
631
|
+
// The SW console line in `logScriptOutcome` is the record.
|
|
632
|
+
if (!next)
|
|
633
|
+
return;
|
|
634
|
+
if (hasReportableOutcome(prior, next)) {
|
|
635
|
+
logScriptEvent(scriptId, { level: 'info', source, message: 'Checking for updates' });
|
|
636
|
+
}
|
|
637
|
+
// Before the state verdict, and not gated by it: a script can be
|
|
638
|
+
// `invalid` for an unrelated reason and still have picked up a local
|
|
639
|
+
// stand-in this pass, and that substitution is exactly what the user
|
|
640
|
+
// needs to know about while debugging the failure.
|
|
641
|
+
for (const entry of localFileLogMessages(prior, next)) {
|
|
642
|
+
logScriptEvent(scriptId, { ...entry, source });
|
|
643
|
+
}
|
|
644
|
+
const nextState = loadStateLogEntry(next);
|
|
645
|
+
const priorState = prior ? loadStateLogEntry(prior) : null;
|
|
646
|
+
if (nextState) {
|
|
647
|
+
// Written even when the failure is byte-identical to the one
|
|
648
|
+
// already in the log: every check re-runs the full fetch + parse +
|
|
649
|
+
// validate — an `invalid` row can't take the size+mtime skip, which
|
|
650
|
+
// requires `status === 'ok'` — so this is a fresh verdict on a
|
|
651
|
+
// fresh read, and the check that produced it has to end somewhere.
|
|
652
|
+
logScriptEvent(scriptId, { ...nextState, source });
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
if (prior && priorState) {
|
|
656
|
+
// The row is healthy again. Log the recovery so the Log explains
|
|
657
|
+
// why the badge went back to OK — without this the last word on the
|
|
658
|
+
// script would be a failure that is no longer true.
|
|
659
|
+
logScriptEvent(scriptId, { level: 'info', state: 'ok', source, message: recoveryMessage(prior) });
|
|
660
|
+
// Recovering IS the outcome of this check. Falling through to the
|
|
661
|
+
// uneventful line below would read as a contradiction when the
|
|
662
|
+
// recovery didn't come with new bytes (a dependency host that
|
|
663
|
+
// starts answering again clears the failure without touching the
|
|
664
|
+
// script's own content, so `loadedAt` is preserved unless the
|
|
665
|
+
// dependency's bytes changed meanwhile).
|
|
666
|
+
if (prior.loadedAt === next.loadedAt)
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
if (!prior || prior.loadedAt !== next.loadedAt) {
|
|
670
|
+
// The script's own file is the same, so its size is too. The changed
|
|
671
|
+
// files have their own lines when they are local copies
|
|
672
|
+
// (`localFileLogMessages`).
|
|
673
|
+
const dependenciesOnly = dependenciesOnlyChange(prior, next);
|
|
674
|
+
const size = next.size != null && !dependenciesOnly ? ` (${formatBytes(next.size)})` : '';
|
|
675
|
+
// A new version is a clean slate for the WARNING / ERROR badge: the
|
|
676
|
+
// old version's runtime errors say nothing about this one, and a
|
|
677
|
+
// developer who just fixed one needs the badge to reflect only what
|
|
678
|
+
// happens next. Shown as OK in the Log's status column for the same
|
|
679
|
+
// reason. See `LogEntry.resetsLogLevel`.
|
|
680
|
+
logScriptEvent(scriptId, {
|
|
681
|
+
level: 'info',
|
|
682
|
+
state: 'ok',
|
|
683
|
+
resetsLogLevel: true,
|
|
684
|
+
source,
|
|
685
|
+
message: dependenciesOnly ? 'Loaded new content for its @require or @resource files' : `Loaded new content${size}`,
|
|
686
|
+
});
|
|
687
|
+
// The new content's `@version` caught up with a probe that had been
|
|
688
|
+
// ahead of us. The probes ride along unchanged on the refreshed row
|
|
689
|
+
// (see `preserveConcurrentEdits` in script-store.ts), so the comparison is
|
|
690
|
+
// between the old and new header against the same stored answer.
|
|
691
|
+
if (prior && scriptOutdatedProbe(prior) && !scriptOutdatedProbe(next)) {
|
|
692
|
+
logUpToDateAgain(scriptId, next.header ? getValue(next.header, 'version') : undefined);
|
|
693
|
+
}
|
|
694
|
+
// Replay parse-time diagnostics whenever a new version lands. They
|
|
695
|
+
// appear AFTER the "Loaded new content" line so it's clear which
|
|
696
|
+
// version the warning belongs to. A dependency-only change too: the
|
|
697
|
+
// line above starts the badge over, and the header's warnings still
|
|
698
|
+
// hold (a row that was invalid on a failed `@require` didn't log
|
|
699
|
+
// them on any refresh while it stayed so).
|
|
700
|
+
for (const entry of next.startupLogs ?? []) {
|
|
701
|
+
logScriptEvent(scriptId, startupLogEntry(entry));
|
|
702
|
+
}
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
if (mode === 'manual') {
|
|
706
|
+
logScriptEvent(scriptId, { level: 'info', source, message: NO_CHANGE_MESSAGE });
|
|
707
|
+
// A manual refresh is the user asking what state this script is in,
|
|
708
|
+
// so a parse-time problem that is STILL TRUE gets restated — the
|
|
709
|
+
// same reason `replayScriptLogsAtBoot` restates load state and
|
|
710
|
+
// OUTDATED rather than leaving a badge over a silent Log. Without
|
|
711
|
+
// it the only line a permanently misconfigured script ever gets is
|
|
712
|
+
// the one from the refresh that first loaded it: it scrolls away,
|
|
713
|
+
// Clear log wipes it, and nothing brings it back until the next
|
|
714
|
+
// Chrome restart. Manual only, like the line above — a background
|
|
715
|
+
// check nobody asked for stays silent.
|
|
716
|
+
for (const entry of next.startupLogs ?? []) {
|
|
717
|
+
logScriptEvent(scriptId, startupLogEntry(entry));
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
// Fan a COLLECTION-level fact out into the Log of every script under
|
|
722
|
+
// that collection. Used for the failures that stop us from checking any
|
|
723
|
+
// of them (an unreadable listing, a throw out of the refresh) and for
|
|
724
|
+
// the matching recovery.
|
|
725
|
+
//
|
|
726
|
+
// **Always `info`, never warning/error.** These entries exist so a user
|
|
727
|
+
// reading one script's Log can see why it wasn't checked — the badge
|
|
728
|
+
// for a collection-level problem belongs on the collection row, which
|
|
729
|
+
// already renders `scanError` (see `renderCollectionSection` in
|
|
730
|
+
// collections.ts). Logging them higher would badge every script under
|
|
731
|
+
// the collection WARNING via the `loglevel:` sentinel, which is for
|
|
732
|
+
// things the script itself did.
|
|
733
|
+
//
|
|
734
|
+
// Plain append. A collection that stays unreadable re-logs on each pass,
|
|
735
|
+
// which is honest — every one of those passes really did try to read
|
|
736
|
+
// it. Only reachable from a Refresh / Refresh All click (see the
|
|
737
|
+
// `updateCollection` / `updateAllCollections` callers in
|
|
738
|
+
// collections.ts), so the user asked for each repeat.
|
|
739
|
+
function logCollectionEventToScripts(scripts, mode, message) {
|
|
740
|
+
for (const s of scripts) {
|
|
741
|
+
logScriptEvent(s.id, { level: 'info', source: refreshLogSource(mode), message });
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
// The Log tab's Source label for a refresh pass. Spelled out per mode
|
|
745
|
+
// rather than composed from the mode name: these strings are read by
|
|
746
|
+
// users, and "opportunistic" describes our scheduling policy rather
|
|
747
|
+
// than anything they did or would recognise.
|
|
748
|
+
function refreshLogSource(mode) {
|
|
749
|
+
return mode === 'manual' ? 'manual-refresh' : 'background-refresh';
|
|
750
|
+
}
|
|
751
|
+
// The single line a check writes when it has nothing to report: names
|
|
752
|
+
// the check AND its result, so a routine poll costs one row rather than
|
|
753
|
+
// a start/outcome pair. Manual-only (see `emitScriptOutcomeLog`).
|
|
754
|
+
const NO_CHANGE_MESSAGE = 'Checked for updates and found no change';
|
|
755
|
+
// Whether this check has anything to say beyond that one line — i.e.
|
|
756
|
+
// whether there was something to check for this script in the first
|
|
757
|
+
// place.
|
|
758
|
+
//
|
|
759
|
+
// Mirrors the branches of `emitScriptOutcomeLog` below, in the same
|
|
760
|
+
// order, and is only ever read by it. Kept as its own predicate so the
|
|
761
|
+
// "should we announce the check?" decision and the "what do we report?"
|
|
762
|
+
// decision are written once each rather than twice.
|
|
763
|
+
//
|
|
764
|
+
// True when: it's failing now, it was failing before (so this pass
|
|
765
|
+
// genuinely re-fetched and re-validated — an `invalid` row can't take
|
|
766
|
+
// the size+mtime skip), or the content changed. False only for the
|
|
767
|
+
// healthy-and-unchanged case, whose whole outcome is the one
|
|
768
|
+
// self-describing `NO_CHANGE_MESSAGE` row.
|
|
769
|
+
function hasReportableOutcome(prior, next) {
|
|
770
|
+
if (loadStateLogEntry(next))
|
|
771
|
+
return true;
|
|
772
|
+
if (prior && loadStateLogEntry(prior))
|
|
773
|
+
return true;
|
|
774
|
+
// A local stand-in that appeared, vanished, or changed on disk is an
|
|
775
|
+
// outcome even when the script's own bytes didn't move — that's the
|
|
776
|
+
// common shape while editing a required library.
|
|
777
|
+
if (localFileLogMessages(prior, next).length > 0)
|
|
778
|
+
return true;
|
|
779
|
+
return !prior || prior.loadedAt !== next.loadedAt;
|
|
780
|
+
}
|
|
781
|
+
// How to phrase "the failure we last logged for this row is over".
|
|
782
|
+
// Keyed off the prior row's state so the line names what recovered —
|
|
783
|
+
// a bare "ok again" wouldn't tell the user whether the file came back,
|
|
784
|
+
// the parse started succeeding, or the server did.
|
|
785
|
+
function recoveryMessage(prior) {
|
|
786
|
+
if (prior.status === 'invalid')
|
|
787
|
+
return 'Loads cleanly again (was invalid)';
|
|
788
|
+
return 'Refresh succeeded again (collection is reachable)';
|
|
789
|
+
}
|
|
790
|
+
// Sentinel key in chrome.storage.session that records "we've already
|
|
791
|
+
// replayed the persisted per-script state for THIS Chrome session."
|
|
792
|
+
// session storage is cleared on Chrome shutdown, so a fresh Chrome
|
|
793
|
+
// session always misses the sentinel; SW restarts within a session
|
|
794
|
+
// always hit it.
|
|
795
|
+
//
|
|
796
|
+
// The key name predates the load-state leg and now under-describes what
|
|
797
|
+
// it gates. Kept as-is deliberately: renaming it would make one Chrome
|
|
798
|
+
// session miss its existing sentinel and replay a second time, which
|
|
799
|
+
// buys nothing.
|
|
800
|
+
const BOOT_REPLAY_SENTINEL_KEY = 'sm:startup-logs-replayed-at';
|
|
801
|
+
// Refill the Log tab from persisted state, once per Chrome session, at
|
|
802
|
+
// SW startup. Everything replayed here is so for the same reason: the
|
|
803
|
+
// facts live in `chrome.storage.local` and outlive the Chrome shutdown
|
|
804
|
+
// that wipes the session-scoped log, so without this a fresh session
|
|
805
|
+
// shows a red or amber badge over an empty Log until the user manually
|
|
806
|
+
// refreshes.
|
|
807
|
+
//
|
|
808
|
+
// - **Load state** — a row that is currently `invalid` / `missing` /
|
|
809
|
+
// carrying a `refreshError`. Nothing re-checks a script until it is
|
|
810
|
+
// refreshed or its page is visited, so without this a script broken
|
|
811
|
+
// since before the restart shows a badge over an empty Log.
|
|
812
|
+
// - **Outdated** — a row whose persisted `versionProbes` put an
|
|
813
|
+
// upstream ahead of it. The probe only logs the *transition*, and a
|
|
814
|
+
// re-probe after the restart finds the same answer, so this is the
|
|
815
|
+
// only line the OUTDATED badge would ever get.
|
|
816
|
+
// - **Parse-time diagnostics** — `script.startupLogs`.
|
|
817
|
+
//
|
|
818
|
+
// The sentinel makes it idempotent: SW restarts within the same Chrome
|
|
819
|
+
// session bail out immediately. Every leg reuses the exact message
|
|
820
|
+
// text the refresh paths use, so the Log tab is visually uniform.
|
|
821
|
+
//
|
|
822
|
+
// The sentinel is written AFTER the walk, not before: MV3 can evict the
|
|
823
|
+
// worker mid-loop, and claiming it up front would mark the session
|
|
824
|
+
// replayed while most scripts still had empty logs — the exact state
|
|
825
|
+
// this exists to prevent. A torn-down replay simply runs again on the
|
|
826
|
+
// next SW wake-up, duplicating a row for the scripts the first pass
|
|
827
|
+
// reached. Rare and bounded to one Chrome session.
|
|
828
|
+
export async function replayScriptLogsAtBoot() {
|
|
829
|
+
const got = await chrome.storage.session.get(BOOT_REPLAY_SENTINEL_KEY);
|
|
830
|
+
if (got[BOOT_REPLAY_SENTINEL_KEY])
|
|
831
|
+
return;
|
|
832
|
+
// One read of the script rows (no bodies) via getAllScriptMetas —
|
|
833
|
+
// cheaper than per-collection reads when most scripts won't have
|
|
834
|
+
// anything to replay anyway. Per-script work runs concurrently;
|
|
835
|
+
// `appendScriptLog`'s own queue keeps each script's appends ordered.
|
|
836
|
+
const scripts = await getAllScriptMetas();
|
|
837
|
+
await Promise.all(scripts.map(async (script) => {
|
|
838
|
+
await ensureLoadStateInLogTab(script);
|
|
839
|
+
await ensureOutdatedInLogTab(script);
|
|
840
|
+
await ensureStartupLogsInLogTab(script);
|
|
841
|
+
}));
|
|
842
|
+
await chrome.storage.session.set({ [BOOT_REPLAY_SENTINEL_KEY]: Date.now() });
|
|
843
|
+
}
|
|
844
|
+
// Restate a script's current load failure, if it has one.
|
|
845
|
+
//
|
|
846
|
+
// `source: 'extension-startup'` rather than a `*-refresh` label so the
|
|
847
|
+
// Log reads honestly: nothing was re-checked, we're just carrying the
|
|
848
|
+
// last known verdict forward across a browser restart. Not plain
|
|
849
|
+
// 'startup' either — that would read as the *script's* startup, which
|
|
850
|
+
// is what the `parser` source already covers.
|
|
851
|
+
//
|
|
852
|
+
// Plain append. The session sentinel already makes this run once per
|
|
853
|
+
// Chrome session, which is the only thing keeping the volume down that
|
|
854
|
+
// needs to.
|
|
855
|
+
async function ensureLoadStateInLogTab(script) {
|
|
856
|
+
const entry = loadStateLogEntry(script);
|
|
857
|
+
if (!entry)
|
|
858
|
+
return;
|
|
859
|
+
await appendScriptLog(script.id, { ...entry, source: 'extension-startup' });
|
|
860
|
+
}
|
|
861
|
+
// Restate OUTDATED for a row whose stored probe is still ahead of it.
|
|
862
|
+
// Same source and reasoning as `ensureLoadStateInLogTab`; same wording
|
|
863
|
+
// as the probe's own transition row.
|
|
864
|
+
//
|
|
865
|
+
// `ok` rows only, mirroring `probeOneScript`: an `invalid` / `missing`
|
|
866
|
+
// row keeps its stale probes but isn't running, so "(running <v>)"
|
|
867
|
+
// would be false and the line would land after — and appear to
|
|
868
|
+
// override — the load-state line the row actually deserves.
|
|
869
|
+
async function ensureOutdatedInLogTab(script) {
|
|
870
|
+
if (script.status !== 'ok')
|
|
871
|
+
return;
|
|
872
|
+
const probe = scriptOutdatedProbe(script);
|
|
873
|
+
if (!probe)
|
|
874
|
+
return;
|
|
875
|
+
const loadedVersion = script.header ? getValue(script.header, 'version') : undefined;
|
|
876
|
+
await appendScriptLog(script.id, { ...outdatedLogEntry(probe, loadedVersion), source: 'extension-startup' });
|
|
877
|
+
}
|
|
878
|
+
// Replay `script.startupLogs` into the Log tab as `parser`-source rows.
|
|
879
|
+
//
|
|
880
|
+
// Plain append, like everything else here. A refresh that races this
|
|
881
|
+
// replay can leave a duplicate pair of rows — bounded to once per
|
|
882
|
+
// script per Chrome session, and a visible duplicate is a cheaper
|
|
883
|
+
// problem than a rule that has to explain when a line is dropped.
|
|
884
|
+
async function ensureStartupLogsInLogTab(script) {
|
|
885
|
+
for (const sl of script.startupLogs ?? []) {
|
|
886
|
+
await appendScriptLog(script.id, startupLogEntry(sl));
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
// Roll a collection-level refresh up into one SW-console line, and fan
|
|
890
|
+
// the per-script outcomes out into the Log tab.
|
|
891
|
+
//
|
|
892
|
+
// The scan-failure case is handled separately from the per-script loop.
|
|
893
|
+
// `refreshCollection` deliberately leaves every script row untouched
|
|
894
|
+
// when the listing can't be read (see [docs/storage.md]) — so the
|
|
895
|
+
// per-script loop would report no change for every one of them, which
|
|
896
|
+
// is a lie: nothing was checked. Instead the reason is fanned out to
|
|
897
|
+
// each script (see `logCollectionEventToScripts` for why that's
|
|
898
|
+
// `info`).
|
|
899
|
+
//
|
|
900
|
+
// Exported for unit tests.
|
|
901
|
+
export function summarizeCollectionOutcome(label, prior, next, mode) {
|
|
902
|
+
const nextScripts = next.scripts;
|
|
903
|
+
const scanError = next.collection.scanError;
|
|
904
|
+
if (scanError) {
|
|
905
|
+
logCollectionEventToScripts(nextScripts, mode,
|
|
906
|
+
// Same no-echo phrasing as the collections page's banner, since
|
|
907
|
+
// `scanError` usually starts with its own `Could not read …`.
|
|
908
|
+
// A message that already ends its sentence (a GitHub hint) gets no
|
|
909
|
+
// second period.
|
|
910
|
+
`This collection did not load: ${/[.!?]$/.test(scanError) ? scanError : `${scanError}.`} Still running the cached copy.`);
|
|
911
|
+
console.log(`[SourceMonkey] collection refresh: ${label}: scan failed: ${scanError}`);
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
if (prior?.collection.scanError) {
|
|
915
|
+
logCollectionEventToScripts(nextScripts, mode, 'This collection is readable again');
|
|
916
|
+
}
|
|
917
|
+
const priorBy = prior?.scripts ?? new Map();
|
|
918
|
+
let changed = 0;
|
|
919
|
+
let added = 0;
|
|
920
|
+
let unchanged = 0;
|
|
921
|
+
let errored = 0;
|
|
922
|
+
let refreshFailed = 0;
|
|
923
|
+
for (const n of nextScripts) {
|
|
924
|
+
const prior = priorBy.get(n.id);
|
|
925
|
+
emitScriptOutcomeLog(n.id, prior, n, mode);
|
|
926
|
+
if (n.status === 'invalid') {
|
|
927
|
+
errored++;
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
// A still-running row whose latest refresh failed counts as its own
|
|
931
|
+
// bucket, not "unchanged" — the content is unchanged but the outcome
|
|
932
|
+
// wasn't a clean check.
|
|
933
|
+
if (n.refreshError) {
|
|
934
|
+
refreshFailed++;
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
if (!prior) {
|
|
938
|
+
added++;
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
if (prior.loadedAt !== n.loadedAt)
|
|
942
|
+
changed++;
|
|
943
|
+
else
|
|
944
|
+
unchanged++;
|
|
945
|
+
}
|
|
946
|
+
const parts = [`${nextScripts.length} script(s)`];
|
|
947
|
+
if (added)
|
|
948
|
+
parts.push(`${added} new`);
|
|
949
|
+
if (changed)
|
|
950
|
+
parts.push(`${changed} updated`);
|
|
951
|
+
if (unchanged)
|
|
952
|
+
parts.push(`${unchanged} unchanged`);
|
|
953
|
+
if (refreshFailed)
|
|
954
|
+
parts.push(`${refreshFailed} refresh-failed`);
|
|
955
|
+
if (errored)
|
|
956
|
+
parts.push(`${errored} error`);
|
|
957
|
+
console.log(`[SourceMonkey] collection refresh: ${label}: ${parts.join(', ')}`);
|
|
958
|
+
}
|
|
959
|
+
function describe(err) {
|
|
960
|
+
return err instanceof Error ? err.message : String(err);
|
|
961
|
+
}
|
|
962
|
+
// Convenience: kick off opportunistic checks for a batch of script IDs
|
|
963
|
+
// without awaiting. Each id gates independently; ids whose check is
|
|
964
|
+
// rate-limited or already in flight are dropped silently. Used by the
|
|
965
|
+
// background SW when a navigation matches one or more scripts.
|
|
966
|
+
//
|
|
967
|
+
// **This is a fan-out over one page's script set, so it always gets a
|
|
968
|
+
// batch** — the caller's when they have one (the same navigation's
|
|
969
|
+
// reload fast path, so the two halves of one commit share a single
|
|
970
|
+
// collection read and their file reads rather than one each), otherwise
|
|
971
|
+
// its own. The rule is uniform on purpose: making it depend on which
|
|
972
|
+
// call reached `gate()` first is exactly the ordering-sensitive shape
|
|
973
|
+
// `parseCheck` had to be fixed out of. See `updateScript`.
|
|
974
|
+
export function requestOpportunisticScriptChecks(scriptIds, batch = createRefreshBatch()) {
|
|
975
|
+
for (const id of scriptIds) {
|
|
976
|
+
// Swallow rejections — opportunistic work shouldn't ever surface a
|
|
977
|
+
// user-visible error; the failure was already logged by the fetch.
|
|
978
|
+
//
|
|
979
|
+
// `'defer'`: navigation-triggered, so it doesn't wait for the parse
|
|
980
|
+
// verdict — and this call and the reload fast path's coalesce onto
|
|
981
|
+
// whichever of them reaches the gate first. See `updateScript`.
|
|
982
|
+
updateScript(id, 'opportunistic', 'if-stale', 'defer', batch).catch(() => { });
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
// Test/debug hook so tests can reset the rate-limiter without
|
|
986
|
+
// timing-mock gymnastics.
|
|
987
|
+
export function _resetUpdaterStateForTests() {
|
|
988
|
+
scriptState.clear();
|
|
989
|
+
collectionState.clear();
|
|
990
|
+
allCollectionsState.clear();
|
|
991
|
+
}
|