@hraness/ghostget 0.17.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +458 -0
- package/LICENSE +21 -0
- package/README.md +1359 -0
- package/bunfig.toml +2 -0
- package/dist/apple-photos-client.js +704 -0
- package/dist/beeper-client.js +4602 -0
- package/dist/client.js +1605 -0
- package/dist/index-26yq8q16.js +31 -0
- package/dist/index-9t13b4k7.js +508 -0
- package/dist/index-axwm9b3v.js +5 -0
- package/dist/index-gwk7rbyj.js +30 -0
- package/dist/index.js +701 -0
- package/dist/messaging.js +1130 -0
- package/dist/omni-client.js +1406 -0
- package/dist/whatsapp-client.js +355 -0
- package/docs/imessage-direct-provider.md +106 -0
- package/docs/rental-listings.md +94 -0
- package/package.json +600 -0
- package/skills/ghostget/SKILL.md +251 -0
- package/skills/ghostget/agents/openai.yaml +4 -0
- package/skills/ghostget/references/adapter-contract.md +181 -0
- package/skills/ghostget/references/apple-photos.md +115 -0
- package/skills/ghostget/references/article-drafts.md +235 -0
- package/skills/ghostget/references/code-owned-provider-scaffold.md +134 -0
- package/skills/ghostget/references/cross-posting-video.md +139 -0
- package/skills/ghostget/references/cross-posting.md +82 -0
- package/skills/ghostget/references/derivation.md +390 -0
- package/skills/ghostget/references/hraness-social-profile-stats.json +269 -0
- package/skills/ghostget/references/install.md +66 -0
- package/skills/ghostget/references/linkedin-adapter.md +442 -0
- package/skills/ghostget/references/messaging.md +168 -0
- package/skills/ghostget/references/meta-comet-contract.md +153 -0
- package/skills/ghostget/references/platform-patterns.md +122 -0
- package/skills/ghostget/references/provider-plugins.md +496 -0
- package/skills/ghostget/references/reddit-flair.md +78 -0
- package/skills/ghostget/references/safety-and-state.md +214 -0
- package/skills/ghostget/references/settlement-and-duplicate-cleanup.md +79 -0
- package/skills/ghostget/references/social-platform-routing.md +66 -0
- package/skills/ghostget/references/social-profile-stats.md +195 -0
- package/skills/ghostget/references/social-video-platform-routing.md +89 -0
- package/skills/ghostget/references/whatsapp-adapter.md +101 -0
- package/skills/ghostget/references/x-adapter.md +332 -0
- package/skills/ghostget/references/x-ai-disclosure.md +71 -0
- package/src/apple-photos-cli.ts +639 -0
- package/src/apple-photos-client-types.ts +168 -0
- package/src/apple-photos-client.ts +201 -0
- package/src/apple-photos-contact-evidence.ts +838 -0
- package/src/apple-photos-local-source.ts +1131 -0
- package/src/args.ts +2053 -0
- package/src/article-draft-document.ts +417 -0
- package/src/article-draft-embeds.ts +123 -0
- package/src/article-draft-images.ts +214 -0
- package/src/assets/adapter-template/wrench-adapter.json +40 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.json +1500 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v1.0.0.json +129 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v1.1.0.json +207 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v2.0.0.json +1463 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v2.1.0.json +1463 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v2.2.0.json +1475 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v2.3.0.json +1488 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.json +684 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.0.0.json +581 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.1.0.json +581 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.2.0.json +581 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.3.0.json +615 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.4.0.json +684 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.5.0.json +684 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.6.0.json +684 -0
- package/src/assets/adapters/clasificados/wrench-web-adapter.json +54 -0
- package/src/assets/adapters/facebook/wrench-web-adapter.json +750 -0
- package/src/assets/adapters/facebook/wrench-web-adapter.v1.0.0.json +706 -0
- package/src/assets/adapters/facebook/wrench-web-adapter.v1.1.0.json +703 -0
- package/src/assets/adapters/facebook/wrench-web-adapter.v1.2.0.json +750 -0
- package/src/assets/adapters/facebook-group/wrench-web-adapter.json +682 -0
- package/src/assets/adapters/facebook-group/wrench-web-adapter.v1.0.0.json +583 -0
- package/src/assets/adapters/facebook-group/wrench-web-adapter.v1.1.0.json +682 -0
- package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.json +437 -0
- package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.v1.0.0.json +383 -0
- package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.v1.1.0.json +437 -0
- package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.v1.1.1.json +437 -0
- package/src/assets/adapters/facebook-page/wrench-web-adapter.json +880 -0
- package/src/assets/adapters/facebook-page/wrench-web-adapter.v1.0.0.json +740 -0
- package/src/assets/adapters/facebook-page/wrench-web-adapter.v1.1.0.json +880 -0
- package/src/assets/adapters/github/wrench-web-adapter.json +69 -0
- package/src/assets/adapters/github/wrench-web-adapter.v1.0.0.json +42 -0
- package/src/assets/adapters/github/wrench-web-adapter.v1.1.0.json +69 -0
- package/src/assets/adapters/gmail/wrench-adapter.json +161 -0
- package/src/assets/adapters/gmail/wrench-adapter.v1.2.0.json +157 -0
- package/src/assets/adapters/hacker-news/wrench-web-adapter.json +315 -0
- package/src/assets/adapters/hacker-news/wrench-web-adapter.v1.0.0.json +315 -0
- package/src/assets/adapters/imessage/wrench-web-adapter.json +236 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.json +719 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.0.0.json +627 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.1.0.json +609 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.2.0.json +640 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.3.0.json +671 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.4.0.json +710 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.5.0.json +710 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.6.0.json +719 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.7.0.json +719 -0
- package/src/assets/adapters/linkedin/wrench-adapter.json +442 -0
- package/src/assets/adapters/linkedin/wrench-adapter.v0.4.0.json +124 -0
- package/src/assets/adapters/linkedin/wrench-adapter.v1.0.0.json +411 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.json +914 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.0.0.json +604 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.1.0.json +604 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.10.0.json +809 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.11.0.json +809 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.12.0.json +820 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.13.0.json +819 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.14.0.json +819 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.15.0.json +819 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.16.0.json +874 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.17.0.json +874 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.18.0.json +874 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.19.0.json +874 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.2.0.json +731 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.3.0.json +768 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.4.0.json +808 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.5.0.json +808 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.6.0.json +808 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.7.0.json +825 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.8.0.json +792 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.9.0.json +809 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.json +855 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.0.0.json +577 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.1.0.json +604 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.10.0.json +712 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.11.0.json +712 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.12.0.json +712 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.2.0.json +653 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.3.0.json +712 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.4.0.json +713 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.5.0.json +712 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.6.0.json +713 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.7.0.json +713 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.8.0.json +713 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.9.0.json +713 -0
- package/src/assets/adapters/substack/wrench-web-adapter.json +936 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.0.0.json +817 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.1.0.json +812 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.2.0.json +812 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.3.0.json +902 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.4.0.json +936 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.5.0.json +936 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.6.0.json +936 -0
- package/src/assets/adapters/threads/wrench-web-adapter.json +692 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.0.0.json +629 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.1.0.json +623 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.2.0.json +622 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.3.0.json +622 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.4.0.json +622 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.5.0.json +699 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.6.0.json +693 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.7.0.json +692 -0
- package/src/assets/adapters/tiktok/wrench-web-adapter.json +762 -0
- package/src/assets/adapters/tiktok/wrench-web-adapter.v1.0.0.json +681 -0
- package/src/assets/adapters/tiktok/wrench-web-adapter.v1.1.0.json +708 -0
- package/src/assets/adapters/tiktok/wrench-web-adapter.v1.2.0.json +762 -0
- package/src/assets/adapters/tiktok/wrench-web-adapter.v1.3.0.json +762 -0
- package/src/assets/adapters/twitch/wrench-web-adapter.json +42 -0
- package/src/assets/adapters/twitch/wrench-web-adapter.v1.0.0.json +42 -0
- package/src/assets/adapters/whatsapp/wrench-web-adapter.json +400 -0
- package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.0.0.json +355 -0
- package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.1.0.json +386 -0
- package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.2.0.json +386 -0
- package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.3.0.json +400 -0
- package/src/assets/adapters/x/wrench-adapter.json +749 -0
- package/src/assets/adapters/x/wrench-adapter.v1.0.0.json +700 -0
- package/src/assets/adapters/x/wrench-adapter.v1.1.0.json +707 -0
- package/src/assets/adapters/x/wrench-web-adapter.json +712 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.1.0.json +579 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.10.0.json +685 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.11.0.json +712 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.12.0.json +712 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.13.0.json +712 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.2.0.json +578 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.3.0.json +610 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.4.0.json +617 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.5.0.json +634 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.6.0.json +632 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.7.0.json +649 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.8.0.json +649 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.9.0.json +683 -0
- package/src/assets/adapters/youtube/wrench-web-adapter.json +561 -0
- package/src/assets/adapters/youtube/wrench-web-adapter.v1.0.0.json +465 -0
- package/src/assets/adapters/youtube/wrench-web-adapter.v1.1.0.json +519 -0
- package/src/assets/adapters/youtube/wrench-web-adapter.v1.2.0.json +561 -0
- package/src/assets/adapters/youtube/wrench-web-adapter.v1.3.0.json +561 -0
- package/src/assets/code-owned-provider-template/plugin.test.ts.template +54 -0
- package/src/assets/code-owned-provider-template/plugin.ts.template +102 -0
- package/src/assets/code-owned-provider-template/runtime.internal.test.ts.template +44 -0
- package/src/assets/code-owned-provider-template/runtime.ts.template +59 -0
- package/src/assets/code-owned-provider-template/wrench-adapter.json.template +33 -0
- package/src/assets/fonts/nebula-sans/LICENSE.txt +96 -0
- package/src/assets/fonts/nebula-sans/NebulaSans-Book.woff2 +0 -0
- package/src/assets/fonts/nebula-sans/PROVENANCE.md +14 -0
- package/src/assets.d.ts +4 -0
- package/src/auth.ts +1157 -0
- package/src/beeper-client-types.ts +169 -0
- package/src/beeper-client.ts +250 -0
- package/src/beeper-contact-interactions-cli.ts +180 -0
- package/src/beeper-contact-interactions.ts +1612 -0
- package/src/beeper-message-bundle-v1.ts +257 -0
- package/src/beeper-message-like-me-cli.ts +84 -0
- package/src/beeper-message-like-me-export.ts +2274 -0
- package/src/beeper-message-like-me-recovery.ts +931 -0
- package/src/beeper-message-like-me-source.ts +4034 -0
- package/src/browser-admission.ts +928 -0
- package/src/browser-snapshots.ts +260 -0
- package/src/browser.ts +3873 -0
- package/src/canonical-json.ts +60 -0
- package/src/catalog-cli.ts +638 -0
- package/src/cli.ts +208 -0
- package/src/client-types.ts +301 -0
- package/src/client.ts +2726 -0
- package/src/confirmed-write-failure.ts +44 -0
- package/src/confirmed-write-model.ts +72 -0
- package/src/confirmed-write-platform.ts +1055 -0
- package/src/confirmed-write-program.ts +224 -0
- package/src/confirmed-write-runtime.ts +16 -0
- package/src/cursor-token.ts +228 -0
- package/src/derivation-dnr-readiness.ts +367 -0
- package/src/derivation-file-chooser.ts +384 -0
- package/src/derivation-network-boundary.ts +835 -0
- package/src/derivation-network-guard.ts +782 -0
- package/src/derivation-network-proxy-helper.ts +440 -0
- package/src/derivation-network-proxy.ts +385 -0
- package/src/derive-command-helper.ts +693 -0
- package/src/derive-fixtures.ts +343 -0
- package/src/derive-review.ts +980 -0
- package/src/derive.ts +4754 -0
- package/src/fixtures/beeper-message-like-me-v1/accounts.ndjson +2 -0
- package/src/fixtures/beeper-message-like-me-v1/conversations.ndjson +1 -0
- package/src/fixtures/beeper-message-like-me-v1/manifest.json +1 -0
- package/src/fixtures/beeper-message-like-me-v1/messages.ndjson +2 -0
- package/src/fixtures/beeper-message-like-me-v1/participants.ndjson +3 -0
- package/src/fixtures/beeper-message-like-me-v1/reactions.ndjson +1 -0
- package/src/fixtures/beeper-message-like-me-v1/tombstones.ndjson +1 -0
- package/src/ghostget.ts +3672 -0
- package/src/gmail-capture.ts +1244 -0
- package/src/har-internal.ts +1242 -0
- package/src/har.ts +743 -0
- package/src/index.ts +218 -0
- package/src/invocation-read-platform.ts +55 -0
- package/src/invocation-read-program.ts +93 -0
- package/src/invocation-read-runtime.ts +11 -0
- package/src/linked-device-lifecycle-admission.ts +450 -0
- package/src/linked-device-lifecycle-journal.ts +1560 -0
- package/src/linked-device-lifecycle-runtime.ts +1602 -0
- package/src/local-cli-admission.ts +135 -0
- package/src/local-cli-contracts.ts +271 -0
- package/src/local-cli-execution.ts +98 -0
- package/src/local-cli-surface-contract.ts +1510 -0
- package/src/local-cli-tool-identity.ts +336 -0
- package/src/media/archive.ts +2325 -0
- package/src/media/args.ts +303 -0
- package/src/media/cli.ts +287 -0
- package/src/media/doctor.ts +475 -0
- package/src/media/ffmpeg.ts +1330 -0
- package/src/media/http-capture.ts +711 -0
- package/src/media/http-probe.ts +593 -0
- package/src/media/http.ts +387 -0
- package/src/media/index.ts +10 -0
- package/src/media/local-transcription.ts +613 -0
- package/src/media/lock.ts +321 -0
- package/src/media/manifest.ts +1712 -0
- package/src/media/metadata.ts +906 -0
- package/src/media/process.ts +723 -0
- package/src/media/revision.ts +166 -0
- package/src/media/runtime-closure.ts +1063 -0
- package/src/media/source-router.ts +187 -0
- package/src/media/transcriber-config.ts +1122 -0
- package/src/media/transcript-persistence-model.ts +15 -0
- package/src/media/transcript-persistence-platform.ts +89 -0
- package/src/media/transcript-persistence-program.ts +39 -0
- package/src/media/transcript-persistence-runtime.ts +24 -0
- package/src/media/transcript.ts +616 -0
- package/src/media/utf8-order.ts +30 -0
- package/src/media/whisper-cpp.ts +464 -0
- package/src/media/whisper-language.ts +58 -0
- package/src/media/yt-dlp.ts +439 -0
- package/src/message-like-me-agentic-messaging.ts +882 -0
- package/src/messaging-action-store.ts +793 -0
- package/src/messaging-runtime.ts +2136 -0
- package/src/messaging-store.ts +539 -0
- package/src/messaging-types.ts +1663 -0
- package/src/messaging.ts +334 -0
- package/src/model.ts +2671 -0
- package/src/oauth-google.ts +888 -0
- package/src/omni-client-types.ts +306 -0
- package/src/omni-client.ts +2225 -0
- package/src/omni-limits.ts +34 -0
- package/src/omni-model.ts +1614 -0
- package/src/omni-request.ts +379 -0
- package/src/omni-runtime.ts +1632 -0
- package/src/operation-deadline.ts +189 -0
- package/src/path-helper.ts +1656 -0
- package/src/pinned-https.ts +194 -0
- package/src/plan-assets.ts +832 -0
- package/src/platform-catalog.ts +1832 -0
- package/src/plugins/beeper-linked-device/plugin.ts +310 -0
- package/src/plugins/bluesky-web/plugin.ts +178 -0
- package/src/plugins/clasificados-web/plugin.ts +66 -0
- package/src/plugins/github-web/plugin.ts +80 -0
- package/src/plugins/gmail-official/plugin.ts +110 -0
- package/src/plugins/hacker-news-web/plugin.ts +52 -0
- package/src/plugins/imessage-direct/plugin.ts +195 -0
- package/src/plugins/imessage-direct/vendor/0001-fix-keep-AppleScript-send-payloads-out-of-child-argv.patch +1430 -0
- package/src/plugins/imessage-direct/vendor/0002-feat-rpc-add-exact-chat-lookup.patch +225 -0
- package/src/plugins/imessage-direct/vendor/provenance.json +80 -0
- package/src/plugins/linkedin-official/plugin.ts +84 -0
- package/src/plugins/linkedin-web/plugin.ts +412 -0
- package/src/plugins/meta-web/plugin.ts +317 -0
- package/src/plugins/reddit-web/flair.ts +492 -0
- package/src/plugins/reddit-web/plugin.ts +477 -0
- package/src/plugins/substack-web/plugin.ts +124 -0
- package/src/plugins/tiktok-web/plugin.ts +67 -0
- package/src/plugins/twitch-web/plugin.ts +54 -0
- package/src/plugins/whatsapp-linked-device/plugin.ts +105 -0
- package/src/plugins/x-official/plugin.ts +64 -0
- package/src/plugins/x-web/plugin.ts +488 -0
- package/src/plugins/youtube-web/plugin.ts +101 -0
- package/src/portable-run-recovery.ts +466 -0
- package/src/process-identity.ts +501 -0
- package/src/profile-clone-helper.ts +104 -0
- package/src/provider-catalog-views.ts +51 -0
- package/src/provider-context.ts +69 -0
- package/src/provider-contract-definitions-x.ts +45 -0
- package/src/provider-contract-definitions.ts +948 -0
- package/src/provider-contract-input-gmail.ts +176 -0
- package/src/provider-contract-input-linkedin.ts +77 -0
- package/src/provider-contract-input-x.ts +304 -0
- package/src/provider-contract-planning.ts +23 -0
- package/src/provider-contract-semantic-identity.ts +54 -0
- package/src/provider-contracts.ts +251 -0
- package/src/provider-http.ts +723 -0
- package/src/provider-plugin-auth.ts +36 -0
- package/src/provider-plugin-builtins.ts +229 -0
- package/src/provider-plugin-cleanup-barrier.ts +229 -0
- package/src/provider-plugin-cleanup-execution.ts +79 -0
- package/src/provider-plugin-cleanup-resource.ts +470 -0
- package/src/provider-plugin-contract-identity.ts +707 -0
- package/src/provider-plugin-host.ts +1972 -0
- package/src/provider-plugin-identifiers.ts +60 -0
- package/src/provider-plugin-invocation-lease.ts +889 -0
- package/src/provider-plugin-lifecycle-kernel.ts +566 -0
- package/src/provider-plugin-lifecycle.ts +1455 -0
- package/src/provider-plugin-module-analysis.ts +336 -0
- package/src/provider-plugin-package.ts +2530 -0
- package/src/provider-plugin-portable-authority.ts +106 -0
- package/src/provider-plugin-portable-catalog.ts +94 -0
- package/src/provider-plugin-portable-identity.ts +315 -0
- package/src/provider-plugin-portable-runtime.ts +2057 -0
- package/src/provider-plugin-protocol.ts +1625 -0
- package/src/provider-plugin-registry.ts +3342 -0
- package/src/provider-plugin-store.ts +2155 -0
- package/src/provider-plugin-test-fixture.ts +5 -0
- package/src/provider-plugin.ts +4907 -0
- package/src/provider-plugins.generated.ts +43 -0
- package/src/provider-plugins.ts +8 -0
- package/src/provider-subject.ts +41 -0
- package/src/provider.ts +315 -0
- package/src/providers/beeper-local-runtime.ts +5426 -0
- package/src/providers/beeper-local.ts +2739 -0
- package/src/providers/beeper-messaging.ts +462 -0
- package/src/providers/beeper-omni.ts +1159 -0
- package/src/providers/bluesky-web-runtime.ts +2895 -0
- package/src/providers/bluesky-web.ts +1614 -0
- package/src/providers/clasificados-web-runtime.ts +255 -0
- package/src/providers/clasificados-web.ts +503 -0
- package/src/providers/contact-projection.ts +301 -0
- package/src/providers/github-read-model.ts +94 -0
- package/src/providers/github-read-platform.ts +165 -0
- package/src/providers/github-read-program.ts +97 -0
- package/src/providers/github-web-runtime.ts +320 -0
- package/src/providers/github-web.ts +403 -0
- package/src/providers/gmail-api.ts +2834 -0
- package/src/providers/gmail-omni.ts +830 -0
- package/src/providers/gmail.ts +1130 -0
- package/src/providers/hacker-news-web-runtime.ts +220 -0
- package/src/providers/hacker-news-web.ts +809 -0
- package/src/providers/imessage-direct-install.ts +354 -0
- package/src/providers/imessage-direct-messaging.ts +532 -0
- package/src/providers/imessage-direct-omni.ts +400 -0
- package/src/providers/imessage-direct-runtime.ts +1498 -0
- package/src/providers/imessage-direct.ts +351 -0
- package/src/providers/instagram-video-foundations.ts +913 -0
- package/src/providers/instagram-web-profile-browser.ts +600 -0
- package/src/providers/iso-bmff.ts +338 -0
- package/src/providers/linkedin-company-platform.ts +42 -0
- package/src/providers/linkedin-company-program.ts +88 -0
- package/src/providers/linkedin-contact-failure.ts +84 -0
- package/src/providers/linkedin-contact-platform.ts +45 -0
- package/src/providers/linkedin-contact-program.ts +91 -0
- package/src/providers/linkedin-profile-activity-failure.ts +76 -0
- package/src/providers/linkedin-profile-activity-platform.ts +41 -0
- package/src/providers/linkedin-profile-activity-program.ts +67 -0
- package/src/providers/linkedin-read-failure.ts +52 -0
- package/src/providers/linkedin-self-platform.ts +53 -0
- package/src/providers/linkedin-self-program.ts +122 -0
- package/src/providers/linkedin-web-article-browser.ts +794 -0
- package/src/providers/linkedin-web-bootstrap.ts +146 -0
- package/src/providers/linkedin-web-contact.ts +830 -0
- package/src/providers/linkedin-web-feed-browser.ts +832 -0
- package/src/providers/linkedin-web-feed.ts +864 -0
- package/src/providers/linkedin-web-post-browser.ts +653 -0
- package/src/providers/linkedin-web-profile-browser.ts +683 -0
- package/src/providers/linkedin-web-runtime.ts +2388 -0
- package/src/providers/linkedin-web.ts +3305 -0
- package/src/providers/linkedin.ts +1885 -0
- package/src/providers/meta-bootstrap.ts +1043 -0
- package/src/providers/meta-facebook-group.ts +690 -0
- package/src/providers/meta-marketplace-relay.ts +747 -0
- package/src/providers/meta-omni.ts +373 -0
- package/src/providers/meta-relay-bundle.ts +1049 -0
- package/src/providers/meta-web-descriptors.ts +1740 -0
- package/src/providers/meta-web-runtime.ts +3388 -0
- package/src/providers/meta-web.ts +2931 -0
- package/src/providers/read-failure.ts +139 -0
- package/src/providers/reddit-omni.ts +462 -0
- package/src/providers/reddit-web-runtime.ts +1929 -0
- package/src/providers/reddit-web.ts +1611 -0
- package/src/providers/rental-listings.ts +460 -0
- package/src/providers/substack-omni.ts +406 -0
- package/src/providers/substack-video-mp4.ts +33 -0
- package/src/providers/substack-web-runtime.ts +2812 -0
- package/src/providers/substack-web.ts +1502 -0
- package/src/providers/tiktok-video-mp4.ts +33 -0
- package/src/providers/tiktok-web-runtime.ts +770 -0
- package/src/providers/tiktok-web.ts +2786 -0
- package/src/providers/twitch-web-runtime.ts +226 -0
- package/src/providers/twitch-web.ts +274 -0
- package/src/providers/whatsapp-account-identity.ts +55 -0
- package/src/providers/whatsapp-contact-projection-helper.ts +874 -0
- package/src/providers/whatsapp-contact-projection-protocol.ts +403 -0
- package/src/providers/whatsapp-interaction-projection-helper.ts +1452 -0
- package/src/providers/whatsapp-interaction-projection-protocol.ts +462 -0
- package/src/providers/whatsapp-message-export-projection-protocol.ts +532 -0
- package/src/providers/whatsapp-messaging.ts +132 -0
- package/src/providers/whatsapp-omni.ts +586 -0
- package/src/providers/whatsapp-web-runtime.ts +3108 -0
- package/src/providers/whatsapp-web.ts +1037 -0
- package/src/providers/x-image-provenance.ts +523 -0
- package/src/providers/x-made-with-ai.ts +106 -0
- package/src/providers/x-transaction-id.ts +413 -0
- package/src/providers/x-web-runtime.ts +3485 -0
- package/src/providers/x-web.ts +2332 -0
- package/src/providers/x.ts +1889 -0
- package/src/providers/youtube-web-runtime.ts +1400 -0
- package/src/providers/youtube-web.ts +1221 -0
- package/src/read-admission-runtime.ts +91 -0
- package/src/read-client.ts +521 -0
- package/src/read-effect-platform.ts +7 -0
- package/src/read-effect-runtime.ts +14 -0
- package/src/read-effect.ts +52 -0
- package/src/read-projection-admission.ts +866 -0
- package/src/read-projections.ts +3151 -0
- package/src/recovery.ts +1492 -0
- package/src/reviewed-template.ts +146 -0
- package/src/run-journal.ts +1272 -0
- package/src/runtime.ts +5116 -0
- package/src/scripts/generate-provider-plugin-catalog.ts +121 -0
- package/src/scripts/install-whatsapp-protocol.sh +232 -0
- package/src/scripts/recover-install-stages.ts +1491 -0
- package/src/scripts/resolve-state-home.ts +30 -0
- package/src/scripts/scaffold-web-provider.ts +973 -0
- package/src/scripts/sync-bundled-adapters.ts +695 -0
- package/src/session-secrets.ts +1183 -0
- package/src/state-helper.bunfig.toml +2 -0
- package/src/state-helper.ts +2914 -0
- package/src/storage.ts +3842 -0
- package/src/transport-policy.ts +2 -0
- package/src/usage.ts +182 -0
- package/src/version.ts +2 -0
- package/src/web-session-authentication-policy.ts +136 -0
- package/src/web-session-cleanup-admission.ts +2344 -0
- package/src/web-session-cleanup-join.ts +99 -0
- package/src/web-session-client.ts +996 -0
- package/src/web-session-contract-definitions.ts +820 -0
- package/src/web-session-contract-planning.ts +42 -0
- package/src/web-session-contracts.ts +228 -0
- package/src/web-session-cookies.ts +500 -0
- package/src/web-session-execution.ts +385 -0
- package/src/web-session-http.ts +373 -0
- package/src/web-session-read-errors.ts +19 -0
- package/src/web-session-read-runtime.ts +124 -0
- package/src/web-session-recovery.ts +818 -0
- package/src/web-session-sites.ts +58 -0
- package/src/web-session-template.ts +994 -0
- package/src/web-session.ts +93 -0
- package/src/whatsapp-client-binding.ts +21 -0
- package/src/whatsapp-client-types.ts +75 -0
- package/src/whatsapp-client.ts +395 -0
- package/src/whatsapp-export-coordinate.ts +17 -0
- package/src/whatsapp-message-bundle-v2.ts +195 -0
- package/src/whatsapp-message-like-me-cli.ts +195 -0
- package/src/whatsapp-message-like-me-export.ts +1072 -0
- package/src/whatsapp-message-like-me-source.ts +1012 -0
- package/tsconfig.json +67 -0
|
@@ -0,0 +1,2225 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { types as nodeTypes } from "node:util";
|
|
5
|
+
|
|
6
|
+
import { canonicalJson, sha256 } from "./canonical-json";
|
|
7
|
+
import {
|
|
8
|
+
boundedOmniText,
|
|
9
|
+
OMNI_MAX_CURSOR_CHARACTERS as MAX_CURSOR_CHARACTERS,
|
|
10
|
+
OMNI_MAX_REASON_BYTES as MAX_ERROR_BYTES,
|
|
11
|
+
OMNI_MAX_REQUEST_BYTES as MAX_REQUEST_BYTES,
|
|
12
|
+
OMNI_MAX_RESPONSE_BYTES as MAX_OUTPUT_BYTES,
|
|
13
|
+
OMNI_MAX_SOURCES as MAX_SOURCES,
|
|
14
|
+
OMNI_MAX_VIEW_ENTITIES as MAX_VIEW_ENTITIES,
|
|
15
|
+
} from "./omni-limits";
|
|
16
|
+
import type {
|
|
17
|
+
OmniAttachmentV1,
|
|
18
|
+
OmniConversationV1,
|
|
19
|
+
OmniCoverageSourceStatusV1,
|
|
20
|
+
OmniEntityKindV1,
|
|
21
|
+
OmniEntityV1,
|
|
22
|
+
OmniExactFreshnessV1,
|
|
23
|
+
OmniExactSourceStatusV1,
|
|
24
|
+
OmniMessageV1,
|
|
25
|
+
OmniNormalizationSourceStatusV1,
|
|
26
|
+
OmniNotificationV1,
|
|
27
|
+
OmniParticipantV1,
|
|
28
|
+
OmniViewCacheResult,
|
|
29
|
+
OmniViewIdentity,
|
|
30
|
+
OmniViewLiveResult,
|
|
31
|
+
OmniViewMergedResult,
|
|
32
|
+
OmniViewRequest,
|
|
33
|
+
OmniViewSourceRequest,
|
|
34
|
+
OmniViewSourceStatusV1,
|
|
35
|
+
OmniViewV1,
|
|
36
|
+
ReadOmniViewOptions,
|
|
37
|
+
RevalidatedOmniView,
|
|
38
|
+
RevalidatedOmniViewCurrent,
|
|
39
|
+
RevalidateOmniViewOptions,
|
|
40
|
+
} from "./omni-client-types";
|
|
41
|
+
|
|
42
|
+
type JsonRecord = Record<string, unknown>;
|
|
43
|
+
type JsonSnapshot =
|
|
44
|
+
| null
|
|
45
|
+
| boolean
|
|
46
|
+
| number
|
|
47
|
+
| string
|
|
48
|
+
| readonly JsonSnapshot[]
|
|
49
|
+
| { readonly [key: string]: JsonSnapshot };
|
|
50
|
+
|
|
51
|
+
const MAX_JSON_DEPTH = 64;
|
|
52
|
+
const MAX_JSON_NODES = 100_000;
|
|
53
|
+
const MAX_PARTICIPANTS = 256;
|
|
54
|
+
const MAX_ATTACHMENTS = 64;
|
|
55
|
+
const SYNCHRONOUS_COMMAND_TIMEOUT_MS = 20_000;
|
|
56
|
+
const ASYNC_CONTROL_COMMAND_TIMEOUT_MS = 20_000;
|
|
57
|
+
const CHILD_GRACEFUL_TERMINATION_MS = 40_000;
|
|
58
|
+
const CHILD_FORCEFUL_TERMINATION_MS = 5_000;
|
|
59
|
+
const abortSignalAbortedGetter = (() => {
|
|
60
|
+
const descriptor = Object.getOwnPropertyDescriptor(
|
|
61
|
+
AbortSignal.prototype,
|
|
62
|
+
"aborted",
|
|
63
|
+
);
|
|
64
|
+
const getter = descriptor === undefined
|
|
65
|
+
? undefined
|
|
66
|
+
: Reflect.get(descriptor, "get") as unknown;
|
|
67
|
+
return typeof getter !== "function"
|
|
68
|
+
? undefined
|
|
69
|
+
: (value: unknown): unknown => Reflect.apply(getter, value, []);
|
|
70
|
+
})();
|
|
71
|
+
|
|
72
|
+
type PreparedRequest = {
|
|
73
|
+
readonly json: string;
|
|
74
|
+
readonly invocationDigest: string;
|
|
75
|
+
readonly requestDigest: string;
|
|
76
|
+
readonly sources: readonly {
|
|
77
|
+
readonly adapterId: string;
|
|
78
|
+
readonly operationId: "messaging.list" | "messaging.read";
|
|
79
|
+
readonly authId: string;
|
|
80
|
+
readonly requestInputHash: string;
|
|
81
|
+
}[];
|
|
82
|
+
readonly filter: {
|
|
83
|
+
readonly kinds: readonly OmniEntityV1["kind"][] | null;
|
|
84
|
+
readonly conversationId: string | null;
|
|
85
|
+
readonly unread: boolean | null;
|
|
86
|
+
};
|
|
87
|
+
readonly pageLimit: number;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
type PreparedOptions = {
|
|
91
|
+
readonly cwd: string;
|
|
92
|
+
readonly environment: Readonly<Record<string, string>>;
|
|
93
|
+
readonly headed: boolean;
|
|
94
|
+
readonly signal?: AbortSignal;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
type CommandMode = "cache" | "identity" | "live";
|
|
98
|
+
|
|
99
|
+
type PreparedCommand = {
|
|
100
|
+
readonly arguments: readonly string[];
|
|
101
|
+
readonly input: string;
|
|
102
|
+
readonly request: PreparedRequest;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
type ParsedEnvelope =
|
|
106
|
+
| {
|
|
107
|
+
readonly source: "omni-identity";
|
|
108
|
+
readonly identity: OmniViewIdentity;
|
|
109
|
+
readonly view: null;
|
|
110
|
+
}
|
|
111
|
+
| {
|
|
112
|
+
readonly source: "omni-cache";
|
|
113
|
+
readonly identity: OmniViewIdentity;
|
|
114
|
+
readonly view: OmniViewV1;
|
|
115
|
+
}
|
|
116
|
+
| {
|
|
117
|
+
readonly source: "omni-live";
|
|
118
|
+
readonly identity: OmniViewIdentity;
|
|
119
|
+
readonly view: OmniViewV1;
|
|
120
|
+
}
|
|
121
|
+
| {
|
|
122
|
+
readonly source: "omni-exact-cache";
|
|
123
|
+
readonly identity: OmniViewIdentity;
|
|
124
|
+
readonly view: OmniViewV1;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
type CachedBeforeSource =
|
|
128
|
+
| { readonly status: "provided"; readonly value: OmniViewCacheResult | null }
|
|
129
|
+
| { readonly status: "lookup" };
|
|
130
|
+
|
|
131
|
+
function fail(label: string, message: string): never {
|
|
132
|
+
throw new Error(`${label} ${message}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function record(value: unknown, label: string): JsonRecord {
|
|
136
|
+
if (
|
|
137
|
+
nodeTypes.isProxy(value)
|
|
138
|
+
|| typeof value !== "object"
|
|
139
|
+
|| value === null
|
|
140
|
+
|| Array.isArray(value)
|
|
141
|
+
|| (
|
|
142
|
+
Object.getPrototypeOf(value) !== Object.prototype
|
|
143
|
+
&& Object.getPrototypeOf(value) !== null
|
|
144
|
+
)
|
|
145
|
+
) return fail(label, "must be a plain non-proxy object");
|
|
146
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
147
|
+
const keys = Reflect.ownKeys(descriptors);
|
|
148
|
+
if (keys.some((key) => typeof key !== "string")) {
|
|
149
|
+
return fail(label, "must not contain symbol properties");
|
|
150
|
+
}
|
|
151
|
+
for (const key of keys as string[]) {
|
|
152
|
+
const descriptor = descriptors[key];
|
|
153
|
+
if (
|
|
154
|
+
descriptor === undefined
|
|
155
|
+
|| !descriptor.enumerable
|
|
156
|
+
|| !("value" in descriptor)
|
|
157
|
+
) return fail(`${label}.${key}`, "must be an enumerable data property");
|
|
158
|
+
}
|
|
159
|
+
return value as JsonRecord;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function exactKeys(
|
|
163
|
+
value: JsonRecord,
|
|
164
|
+
required: readonly string[],
|
|
165
|
+
optional: readonly string[],
|
|
166
|
+
label: string,
|
|
167
|
+
): void {
|
|
168
|
+
const actual = Object.keys(value);
|
|
169
|
+
const allowed = new Set([...required, ...optional]);
|
|
170
|
+
const unknown = actual.find((key) => !allowed.has(key));
|
|
171
|
+
if (unknown !== undefined) return fail(`${label}.${unknown}`, "is not reviewed");
|
|
172
|
+
const missing = required.find((key) => !Object.hasOwn(value, key));
|
|
173
|
+
if (missing !== undefined) return fail(`${label}.${missing}`, "is required");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function denseArray(
|
|
177
|
+
value: unknown,
|
|
178
|
+
label: string,
|
|
179
|
+
maximum: number,
|
|
180
|
+
): readonly unknown[] {
|
|
181
|
+
if (
|
|
182
|
+
nodeTypes.isProxy(value)
|
|
183
|
+
|| !Array.isArray(value)
|
|
184
|
+
|| Object.getPrototypeOf(value) !== Array.prototype
|
|
185
|
+
|| value.length > maximum
|
|
186
|
+
) return fail(label, `must be a plain array of at most ${maximum} items`);
|
|
187
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
188
|
+
const keys = Reflect.ownKeys(descriptors);
|
|
189
|
+
const expected = new Set<PropertyKey>([
|
|
190
|
+
"length",
|
|
191
|
+
...Array.from({ length: value.length }, (_unused, index) => String(index)),
|
|
192
|
+
]);
|
|
193
|
+
if (keys.length !== expected.size || keys.some((key) => !expected.has(key))) {
|
|
194
|
+
return fail(label, "must be a dense array without named properties");
|
|
195
|
+
}
|
|
196
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
197
|
+
const descriptor = descriptors[String(index)];
|
|
198
|
+
if (
|
|
199
|
+
descriptor === undefined
|
|
200
|
+
|| !descriptor.enumerable
|
|
201
|
+
|| !("value" in descriptor)
|
|
202
|
+
) return fail(`${label}[${index}]`, "must be an enumerable data property");
|
|
203
|
+
}
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function text(
|
|
208
|
+
value: unknown,
|
|
209
|
+
label: string,
|
|
210
|
+
maximumBytes: number,
|
|
211
|
+
allowEmpty = false,
|
|
212
|
+
): string {
|
|
213
|
+
if (
|
|
214
|
+
typeof value !== "string"
|
|
215
|
+
|| Buffer.byteLength(value, "utf8") > maximumBytes
|
|
216
|
+
|| (!allowEmpty && value.length === 0)
|
|
217
|
+
|| [...value].some((character) => {
|
|
218
|
+
const code = character.codePointAt(0) ?? -1;
|
|
219
|
+
return code === 0
|
|
220
|
+
|| code === 8
|
|
221
|
+
|| code === 11
|
|
222
|
+
|| code === 12
|
|
223
|
+
|| (code >= 14 && code <= 31)
|
|
224
|
+
|| code === 127;
|
|
225
|
+
})
|
|
226
|
+
) return fail(label, "must be bounded text");
|
|
227
|
+
return value;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function nullableText(
|
|
231
|
+
value: unknown,
|
|
232
|
+
label: string,
|
|
233
|
+
maximumBytes: number,
|
|
234
|
+
allowEmpty = false,
|
|
235
|
+
): string | null {
|
|
236
|
+
return value === null
|
|
237
|
+
? null
|
|
238
|
+
: text(value, label, maximumBytes, allowEmpty);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function identifier(value: unknown, label: string): string {
|
|
242
|
+
const result = text(value, label, 64);
|
|
243
|
+
if (!/^[a-z][a-z0-9-]{0,63}$/u.test(result)) {
|
|
244
|
+
return fail(label, "must be lowercase kebab-case");
|
|
245
|
+
}
|
|
246
|
+
return result;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function digest(value: unknown, label: string): string {
|
|
250
|
+
const result = text(value, label, 64);
|
|
251
|
+
if (!/^[a-f0-9]{64}$/u.test(result)) return fail(label, "must be a SHA-256 digest");
|
|
252
|
+
return result;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function localCursor(value: unknown, label: string): string {
|
|
256
|
+
const result = text(value, label, MAX_CURSOR_CHARACTERS);
|
|
257
|
+
if (!result.startsWith("smn1.")) {
|
|
258
|
+
return fail(label, "must be an authenticated local omni cursor");
|
|
259
|
+
}
|
|
260
|
+
const encoded = result.slice("smn1.".length);
|
|
261
|
+
if (encoded === "" || /[^A-Za-z0-9_-]/u.test(encoded)) {
|
|
262
|
+
return fail(label, "must be an authenticated local omni cursor");
|
|
263
|
+
}
|
|
264
|
+
const envelope = Buffer.from(encoded, "base64url");
|
|
265
|
+
if (
|
|
266
|
+
envelope.toString("base64url") !== encoded
|
|
267
|
+
|| envelope.byteLength <= 12 + 16
|
|
268
|
+
) {
|
|
269
|
+
return fail(label, "must be an authenticated local omni cursor");
|
|
270
|
+
}
|
|
271
|
+
return result;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function timestamp(value: unknown, label: string): string {
|
|
275
|
+
const result = text(value, label, 64);
|
|
276
|
+
const parsed = new Date(result);
|
|
277
|
+
if (!Number.isFinite(parsed.getTime()) || parsed.toISOString() !== result) {
|
|
278
|
+
return fail(label, "must be a canonical ISO timestamp");
|
|
279
|
+
}
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function nullableTimestamp(value: unknown, label: string): string | null {
|
|
284
|
+
return value === null ? null : timestamp(value, label);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function integer(
|
|
288
|
+
value: unknown,
|
|
289
|
+
label: string,
|
|
290
|
+
minimum: number,
|
|
291
|
+
maximum: number,
|
|
292
|
+
): number {
|
|
293
|
+
if (
|
|
294
|
+
typeof value !== "number"
|
|
295
|
+
|| !Number.isSafeInteger(value)
|
|
296
|
+
|| value < minimum
|
|
297
|
+
|| value > maximum
|
|
298
|
+
) return fail(label, `must be an integer from ${minimum} through ${maximum}`);
|
|
299
|
+
return value;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function nullableInteger(
|
|
303
|
+
value: unknown,
|
|
304
|
+
label: string,
|
|
305
|
+
minimum: number,
|
|
306
|
+
maximum: number,
|
|
307
|
+
): number | null {
|
|
308
|
+
return value === null ? null : integer(value, label, minimum, maximum);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function nullableBoolean(value: unknown, label: string): boolean | null {
|
|
312
|
+
if (value === null || typeof value === "boolean") return value;
|
|
313
|
+
return fail(label, "must be boolean or null");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function boolean(value: unknown, label: string): boolean {
|
|
317
|
+
if (typeof value === "boolean") return value;
|
|
318
|
+
return fail(label, "must be boolean");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function oneOf<const T extends string>(
|
|
322
|
+
value: unknown,
|
|
323
|
+
values: readonly T[],
|
|
324
|
+
label: string,
|
|
325
|
+
): T {
|
|
326
|
+
if (typeof value !== "string" || !values.includes(value as T)) {
|
|
327
|
+
return fail(label, `must be one of ${values.join(", ")}`);
|
|
328
|
+
}
|
|
329
|
+
return value as T;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Clone caller values without invoking accessors or proxy traps. */
|
|
333
|
+
function snapshotJson(value: unknown, label: string): JsonSnapshot {
|
|
334
|
+
let nodes = 0;
|
|
335
|
+
const ancestors = new WeakSet<object>();
|
|
336
|
+
const completed = new WeakMap<object, JsonSnapshot>();
|
|
337
|
+
const visit = (candidate: unknown, depth: number): JsonSnapshot => {
|
|
338
|
+
nodes += 1;
|
|
339
|
+
if (nodes > MAX_JSON_NODES || depth > MAX_JSON_DEPTH) {
|
|
340
|
+
return fail(label, "exceeds its structural bound");
|
|
341
|
+
}
|
|
342
|
+
if (
|
|
343
|
+
candidate === null
|
|
344
|
+
|| typeof candidate === "boolean"
|
|
345
|
+
|| typeof candidate === "string"
|
|
346
|
+
) return candidate;
|
|
347
|
+
if (typeof candidate === "number") {
|
|
348
|
+
if (!Number.isFinite(candidate)) return fail(label, "must contain only JSON data");
|
|
349
|
+
return candidate;
|
|
350
|
+
}
|
|
351
|
+
if (typeof candidate !== "object") return fail(label, "must contain only JSON data");
|
|
352
|
+
if (nodeTypes.isProxy(candidate)) return fail(label, "must not contain proxies");
|
|
353
|
+
const prior = completed.get(candidate);
|
|
354
|
+
if (prior !== undefined) return prior;
|
|
355
|
+
if (ancestors.has(candidate)) return fail(label, "must not be circular");
|
|
356
|
+
ancestors.add(candidate);
|
|
357
|
+
try {
|
|
358
|
+
if (Array.isArray(candidate)) {
|
|
359
|
+
const values = denseArray(candidate, label, MAX_JSON_NODES);
|
|
360
|
+
const cloned = Object.freeze(values.map((entry) =>
|
|
361
|
+
visit(entry, depth + 1)));
|
|
362
|
+
completed.set(candidate, cloned);
|
|
363
|
+
return cloned;
|
|
364
|
+
}
|
|
365
|
+
const source = record(candidate, label);
|
|
366
|
+
const cloned: JsonRecord = {};
|
|
367
|
+
for (const key of Object.keys(source).sort((left, right) =>
|
|
368
|
+
left.localeCompare(right))) {
|
|
369
|
+
Object.defineProperty(cloned, key, {
|
|
370
|
+
value: visit(source[key], depth + 1),
|
|
371
|
+
enumerable: true,
|
|
372
|
+
configurable: false,
|
|
373
|
+
writable: false,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
const result = Object.freeze(cloned) as JsonSnapshot;
|
|
377
|
+
completed.set(candidate, result);
|
|
378
|
+
return result;
|
|
379
|
+
} finally {
|
|
380
|
+
ancestors.delete(candidate);
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
return visit(value, 0);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function parseRequestSource(value: unknown, index: number): OmniViewSourceRequest {
|
|
387
|
+
const label = `Ghostget omni request.sources[${index}]`;
|
|
388
|
+
const source = record(value, label);
|
|
389
|
+
exactKeys(source, ["adapterId", "operationId", "authId"], ["input"], label);
|
|
390
|
+
const operationId = oneOf(
|
|
391
|
+
source.operationId,
|
|
392
|
+
["messaging.list", "messaging.read"] as const,
|
|
393
|
+
`${label}.operationId`,
|
|
394
|
+
);
|
|
395
|
+
const inputValue = Object.hasOwn(source, "input") ? source.input : {};
|
|
396
|
+
const input = record(inputValue, `${label}.input`);
|
|
397
|
+
return Object.freeze({
|
|
398
|
+
adapterId: identifier(source.adapterId, `${label}.adapterId`),
|
|
399
|
+
operationId,
|
|
400
|
+
authId: identifier(source.authId, `${label}.authId`),
|
|
401
|
+
input,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function prepareRequest(requestValue: OmniViewRequest): PreparedRequest {
|
|
406
|
+
const snapshot = snapshotJson(requestValue, "Ghostget omni request");
|
|
407
|
+
const request = record(snapshot, "Ghostget omni request");
|
|
408
|
+
exactKeys(request, ["schemaVersion", "sources"], ["filter", "page"], "Ghostget omni request");
|
|
409
|
+
if (request.schemaVersion !== 1) return fail("Ghostget omni request.schemaVersion", "must be 1");
|
|
410
|
+
const sources = denseArray(request.sources, "Ghostget omni request.sources", MAX_SOURCES)
|
|
411
|
+
.map(parseRequestSource)
|
|
412
|
+
.sort((left, right) => canonicalJson(left).localeCompare(canonicalJson(right)));
|
|
413
|
+
if (sources.length === 0) return fail("Ghostget omni request.sources", "must not be empty");
|
|
414
|
+
const sourceKeys = sources.map((source) => canonicalJson(source));
|
|
415
|
+
if (new Set(sourceKeys).size !== sourceKeys.length) {
|
|
416
|
+
return fail("Ghostget omni request.sources", "must not repeat an exact source request");
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
let filter: OmniViewRequest["filter"];
|
|
420
|
+
if (request.filter !== undefined) {
|
|
421
|
+
const value = record(request.filter, "Ghostget omni request.filter");
|
|
422
|
+
exactKeys(value, [], ["kinds", "conversationId", "unread"], "Ghostget omni request.filter");
|
|
423
|
+
let kinds: readonly OmniEntityKindV1[] | undefined;
|
|
424
|
+
if (value.kinds !== undefined) {
|
|
425
|
+
const parsed = denseArray(value.kinds, "Ghostget omni request.filter.kinds", 3)
|
|
426
|
+
.map((kind, index) => oneOf(
|
|
427
|
+
kind,
|
|
428
|
+
["conversation", "message", "notification"] as const,
|
|
429
|
+
`Ghostget omni request.filter.kinds[${index}]`,
|
|
430
|
+
));
|
|
431
|
+
if (parsed.length === 0 || new Set(parsed).size !== parsed.length) {
|
|
432
|
+
return fail("Ghostget omni request.filter.kinds", "must be non-empty and unique");
|
|
433
|
+
}
|
|
434
|
+
kinds = Object.freeze([...parsed].sort());
|
|
435
|
+
}
|
|
436
|
+
const conversationId = value.conversationId === undefined
|
|
437
|
+
? undefined
|
|
438
|
+
: digest(value.conversationId, "Ghostget omni request.filter.conversationId");
|
|
439
|
+
if (value.unread !== undefined && typeof value.unread !== "boolean") {
|
|
440
|
+
return fail("Ghostget omni request.filter.unread", "must be boolean");
|
|
441
|
+
}
|
|
442
|
+
filter = Object.freeze({
|
|
443
|
+
...(kinds === undefined ? {} : { kinds }),
|
|
444
|
+
...(conversationId === undefined ? {} : { conversationId }),
|
|
445
|
+
...(value.unread === undefined ? {} : { unread: value.unread }),
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
let page: OmniViewRequest["page"];
|
|
450
|
+
if (request.page !== undefined) {
|
|
451
|
+
const value = record(request.page, "Ghostget omni request.page");
|
|
452
|
+
exactKeys(value, [], ["limit", "cursor"], "Ghostget omni request.page");
|
|
453
|
+
const limit = value.limit === undefined
|
|
454
|
+
? 100
|
|
455
|
+
: integer(value.limit, "Ghostget omni request.page.limit", 1, 500);
|
|
456
|
+
const cursor = value.cursor === undefined
|
|
457
|
+
? undefined
|
|
458
|
+
: localCursor(value.cursor, "Ghostget omni request.page.cursor");
|
|
459
|
+
page = Object.freeze({
|
|
460
|
+
limit,
|
|
461
|
+
...(cursor === undefined ? {} : { cursor }),
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const normalized = Object.freeze({
|
|
466
|
+
schemaVersion: 1 as const,
|
|
467
|
+
sources: Object.freeze(sources),
|
|
468
|
+
...(filter === undefined ? {} : { filter }),
|
|
469
|
+
...(page === undefined ? {} : { page }),
|
|
470
|
+
});
|
|
471
|
+
const json = canonicalJson(normalized);
|
|
472
|
+
if (Buffer.byteLength(json, "utf8") > MAX_REQUEST_BYTES) {
|
|
473
|
+
return fail("Ghostget omni request", "exceeds its byte bound");
|
|
474
|
+
}
|
|
475
|
+
const cursorIndependentRequest = Object.freeze({
|
|
476
|
+
schemaVersion: 1 as const,
|
|
477
|
+
sources: normalized.sources,
|
|
478
|
+
...(filter === undefined ? {} : { filter }),
|
|
479
|
+
...(page === undefined
|
|
480
|
+
? {}
|
|
481
|
+
: { page: Object.freeze({ limit: page.limit ?? 100 }) }),
|
|
482
|
+
});
|
|
483
|
+
return Object.freeze({
|
|
484
|
+
json,
|
|
485
|
+
invocationDigest: sha256(json),
|
|
486
|
+
requestDigest: sha256(canonicalJson(cursorIndependentRequest)),
|
|
487
|
+
sources: Object.freeze(sources.map((source) => Object.freeze({
|
|
488
|
+
adapterId: source.adapterId,
|
|
489
|
+
operationId: source.operationId,
|
|
490
|
+
authId: source.authId,
|
|
491
|
+
requestInputHash: sha256(canonicalJson(source.input ?? {})),
|
|
492
|
+
}))),
|
|
493
|
+
filter: Object.freeze({
|
|
494
|
+
kinds: filter?.kinds ?? null,
|
|
495
|
+
conversationId: filter?.conversationId ?? null,
|
|
496
|
+
unread: filter?.unread ?? null,
|
|
497
|
+
}),
|
|
498
|
+
pageLimit: page?.limit ?? 100,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function environmentName(value: string): string {
|
|
503
|
+
if (value.length === 0 || value.includes("=") || value.includes("\0")) {
|
|
504
|
+
return fail("Ghostget omni client environment name", "is malformed");
|
|
505
|
+
}
|
|
506
|
+
return value;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function defineEnvironmentValue(
|
|
510
|
+
environment: Record<string, string>,
|
|
511
|
+
key: string,
|
|
512
|
+
value: string,
|
|
513
|
+
): void {
|
|
514
|
+
Object.defineProperty(environment, key, {
|
|
515
|
+
value,
|
|
516
|
+
enumerable: true,
|
|
517
|
+
configurable: true,
|
|
518
|
+
writable: true,
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function snapshotEnvironment(value: unknown): Readonly<Record<string, string>> {
|
|
523
|
+
const result = Object.create(null) as Record<string, string>;
|
|
524
|
+
for (const [key, environmentValue] of Object.entries(process.env)) {
|
|
525
|
+
if (typeof environmentValue === "string") {
|
|
526
|
+
defineEnvironmentValue(result, key, environmentValue);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (value === undefined) return Object.freeze(result);
|
|
530
|
+
const overrides = record(value, "Ghostget omni client environment");
|
|
531
|
+
for (const [key, environmentValue] of Object.entries(overrides)) {
|
|
532
|
+
const name = environmentName(key);
|
|
533
|
+
if (environmentValue === undefined) {
|
|
534
|
+
delete result[name];
|
|
535
|
+
} else if (
|
|
536
|
+
typeof environmentValue !== "string"
|
|
537
|
+
|| environmentValue.includes("\0")
|
|
538
|
+
) {
|
|
539
|
+
return fail("Ghostget omni client environment value", "is malformed");
|
|
540
|
+
} else {
|
|
541
|
+
defineEnvironmentValue(result, name, environmentValue);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return Object.freeze(result);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function isBrandedAbortSignal(value: unknown): value is AbortSignal {
|
|
548
|
+
if (
|
|
549
|
+
nodeTypes.isProxy(value)
|
|
550
|
+
|| typeof value !== "object"
|
|
551
|
+
|| value === null
|
|
552
|
+
|| abortSignalAbortedGetter === undefined
|
|
553
|
+
|| Object.getPrototypeOf(value) !== AbortSignal.prototype
|
|
554
|
+
) return false;
|
|
555
|
+
try {
|
|
556
|
+
return typeof abortSignalAbortedGetter(value) === "boolean";
|
|
557
|
+
} catch {
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function snapshotOptions(
|
|
563
|
+
optionsValue: ReadOmniViewOptions,
|
|
564
|
+
revalidation: boolean,
|
|
565
|
+
): PreparedOptions {
|
|
566
|
+
const options = record(optionsValue, "Ghostget omni client options");
|
|
567
|
+
exactKeys(
|
|
568
|
+
options,
|
|
569
|
+
[],
|
|
570
|
+
revalidation ? ["environment", "headed", "signal"] : ["environment"],
|
|
571
|
+
"Ghostget omni client options",
|
|
572
|
+
);
|
|
573
|
+
const headedValue = options.headed;
|
|
574
|
+
const signalValue = options.signal;
|
|
575
|
+
if (headedValue !== undefined && typeof headedValue !== "boolean") {
|
|
576
|
+
return fail("Ghostget omni client headed option", "is malformed");
|
|
577
|
+
}
|
|
578
|
+
if (signalValue !== undefined && !isBrandedAbortSignal(signalValue)) {
|
|
579
|
+
return fail("Ghostget omni client abort signal", "is malformed");
|
|
580
|
+
}
|
|
581
|
+
return Object.freeze({
|
|
582
|
+
cwd: process.cwd(),
|
|
583
|
+
environment: snapshotEnvironment(options.environment),
|
|
584
|
+
headed: headedValue === true,
|
|
585
|
+
...(signalValue === undefined ? {} : { signal: signalValue }),
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function signalAborted(signal: AbortSignal): boolean {
|
|
590
|
+
if (abortSignalAbortedGetter === undefined) return true;
|
|
591
|
+
return abortSignalAbortedGetter(signal) === true;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function throwIfAborted(signal: AbortSignal | undefined): void {
|
|
595
|
+
if (signal === undefined || !signalAborted(signal)) return;
|
|
596
|
+
throw abortError(signal);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function abortError(signal: AbortSignal): Error {
|
|
600
|
+
const reason = signal.reason as unknown;
|
|
601
|
+
if (reason instanceof Error) return reason;
|
|
602
|
+
const error = new Error("Ghostget omni revalidation was aborted");
|
|
603
|
+
error.name = "AbortError";
|
|
604
|
+
return error;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function cliSourcePath(): string {
|
|
608
|
+
const besideSource = fileURLToPath(new URL("./cli.ts", import.meta.url));
|
|
609
|
+
if (existsSync(besideSource)) return besideSource;
|
|
610
|
+
const packagedSource = fileURLToPath(new URL("../src/cli.ts", import.meta.url));
|
|
611
|
+
if (existsSync(packagedSource)) return packagedSource;
|
|
612
|
+
throw new Error("the installed Ghostget CLI source is unavailable");
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function requireBunRuntime(): void {
|
|
616
|
+
if (typeof process.versions.bun !== "string") {
|
|
617
|
+
throw new Error("@hraness/ghostget/omni requires Bun to run the installed Ghostget CLI");
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function preparedCommand(
|
|
622
|
+
request: PreparedRequest,
|
|
623
|
+
mode: CommandMode,
|
|
624
|
+
options: PreparedOptions,
|
|
625
|
+
): PreparedCommand {
|
|
626
|
+
return Object.freeze({
|
|
627
|
+
arguments: Object.freeze([
|
|
628
|
+
cliSourcePath(),
|
|
629
|
+
"omni",
|
|
630
|
+
"read",
|
|
631
|
+
"--input",
|
|
632
|
+
"-",
|
|
633
|
+
...(mode === "cache" ? ["--cache-only"] : []),
|
|
634
|
+
...(mode === "identity" ? ["--identity-only"] : []),
|
|
635
|
+
...(mode === "live" && options.headed ? ["--headed"] : []),
|
|
636
|
+
"--json",
|
|
637
|
+
]),
|
|
638
|
+
input: request.json,
|
|
639
|
+
request,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function boundedMessage(value: string): string {
|
|
644
|
+
return boundedOmniText(value, MAX_ERROR_BYTES).trim();
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function childText(value: unknown, label: string): string {
|
|
648
|
+
if (nodeTypes.isProxy(value)) return fail(label, "must not be a proxy");
|
|
649
|
+
if (typeof value === "string") return value;
|
|
650
|
+
if (Buffer.isBuffer(value)) return Buffer.from(value).toString("utf8");
|
|
651
|
+
return fail(label, "must be UTF-8 text");
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function parseJsonOutput(textValue: string, label: string): unknown {
|
|
655
|
+
if (Buffer.byteLength(textValue, "utf8") > MAX_OUTPUT_BYTES) {
|
|
656
|
+
return fail(label, "exceeds its byte bound");
|
|
657
|
+
}
|
|
658
|
+
try {
|
|
659
|
+
return JSON.parse(textValue) as unknown;
|
|
660
|
+
} catch {
|
|
661
|
+
return fail(label, "is malformed JSON");
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function participant(value: unknown, label: string): OmniParticipantV1 {
|
|
666
|
+
const source = record(value, label);
|
|
667
|
+
exactKeys(source, ["providerId", "displayName", "handle"], [], label);
|
|
668
|
+
return Object.freeze({
|
|
669
|
+
providerId: nullableText(source.providerId, `${label}.providerId`, 1_024),
|
|
670
|
+
displayName: nullableText(source.displayName, `${label}.displayName`, 2_048, true),
|
|
671
|
+
handle: nullableText(source.handle, `${label}.handle`, 512),
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function attachment(value: unknown, label: string): OmniAttachmentV1 {
|
|
676
|
+
const source = record(value, label);
|
|
677
|
+
exactKeys(source, ["kind", "mimeType", "name", "sizeBytes"], [], label);
|
|
678
|
+
return Object.freeze({
|
|
679
|
+
kind: oneOf(
|
|
680
|
+
source.kind,
|
|
681
|
+
["audio", "document", "image", "link", "sticker", "video", "unknown"] as const,
|
|
682
|
+
`${label}.kind`,
|
|
683
|
+
),
|
|
684
|
+
mimeType: nullableText(source.mimeType, `${label}.mimeType`, 256),
|
|
685
|
+
name: nullableText(source.name, `${label}.name`, 2_048, true),
|
|
686
|
+
sizeBytes: nullableInteger(
|
|
687
|
+
source.sizeBytes,
|
|
688
|
+
`${label}.sizeBytes`,
|
|
689
|
+
0,
|
|
690
|
+
Number.MAX_SAFE_INTEGER,
|
|
691
|
+
),
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function entityCommon(source: JsonRecord, label: string) {
|
|
696
|
+
const entitySource = record(source.source, `${label}.source`);
|
|
697
|
+
exactKeys(entitySource, ["surfaceId", "authId", "providerId"], [], `${label}.source`);
|
|
698
|
+
const providerId = text(source.providerId, `${label}.providerId`, 1_024);
|
|
699
|
+
const providerSourceId = text(
|
|
700
|
+
entitySource.providerId,
|
|
701
|
+
`${label}.source.providerId`,
|
|
702
|
+
1_024,
|
|
703
|
+
);
|
|
704
|
+
if (providerSourceId !== providerId) {
|
|
705
|
+
return fail(`${label}.source.providerId`, "does not match providerId");
|
|
706
|
+
}
|
|
707
|
+
return Object.freeze({
|
|
708
|
+
id: digest(source.id, `${label}.id`),
|
|
709
|
+
providerId,
|
|
710
|
+
providerRevision: nullableText(
|
|
711
|
+
source.providerRevision,
|
|
712
|
+
`${label}.providerRevision`,
|
|
713
|
+
1_024,
|
|
714
|
+
),
|
|
715
|
+
orderedAt: nullableTimestamp(source.orderedAt, `${label}.orderedAt`),
|
|
716
|
+
source: Object.freeze({
|
|
717
|
+
surfaceId: text(entitySource.surfaceId, `${label}.source.surfaceId`, 64),
|
|
718
|
+
authId: text(entitySource.authId, `${label}.source.authId`, 64),
|
|
719
|
+
providerId: providerSourceId,
|
|
720
|
+
}),
|
|
721
|
+
conversationId: source.conversationId === null
|
|
722
|
+
? null
|
|
723
|
+
: digest(source.conversationId, `${label}.conversationId`),
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function verifyEntityRevision<T extends Omit<OmniEntityV1, "revision">>(
|
|
728
|
+
semantic: T,
|
|
729
|
+
revisionValue: unknown,
|
|
730
|
+
label: string,
|
|
731
|
+
): T & { readonly revision: string } {
|
|
732
|
+
const revision = digest(revisionValue, `${label}.revision`);
|
|
733
|
+
if (sha256(canonicalJson(semantic)) !== revision) {
|
|
734
|
+
return fail(`${label}.revision`, "does not authenticate its semantic fields");
|
|
735
|
+
}
|
|
736
|
+
return Object.freeze({ ...semantic, revision });
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function conversationEntity(
|
|
740
|
+
source: JsonRecord,
|
|
741
|
+
label: string,
|
|
742
|
+
): OmniConversationV1 {
|
|
743
|
+
const hasConversationKind = Object.hasOwn(source, "conversationKind");
|
|
744
|
+
exactKeys(source, [
|
|
745
|
+
"kind", "providerId", "providerRevision", "orderedAt",
|
|
746
|
+
...(hasConversationKind ? ["conversationKind"] : []), "detail", "title",
|
|
747
|
+
"summary", "participants", "unread", "unreadCount", "archived", "pending",
|
|
748
|
+
"id", "revision", "source", "conversationId",
|
|
749
|
+
], [], label);
|
|
750
|
+
const common = entityCommon(source, label);
|
|
751
|
+
if (common.conversationId !== null) {
|
|
752
|
+
return fail(`${label}.conversationId`, "must be null for conversations");
|
|
753
|
+
}
|
|
754
|
+
const semantic = Object.freeze({
|
|
755
|
+
kind: "conversation" as const,
|
|
756
|
+
...(hasConversationKind
|
|
757
|
+
? {
|
|
758
|
+
conversationKind: oneOf(
|
|
759
|
+
source.conversationKind,
|
|
760
|
+
["single", "group", "unknown"] as const,
|
|
761
|
+
`${label}.conversationKind`,
|
|
762
|
+
),
|
|
763
|
+
}
|
|
764
|
+
: {}),
|
|
765
|
+
providerId: common.providerId,
|
|
766
|
+
providerRevision: common.providerRevision,
|
|
767
|
+
orderedAt: common.orderedAt,
|
|
768
|
+
detail: oneOf(source.detail, ["summary", "full"] as const, `${label}.detail`),
|
|
769
|
+
title: nullableText(source.title, `${label}.title`, 4_096, true),
|
|
770
|
+
summary: nullableText(source.summary, `${label}.summary`, 64 * 1024, true),
|
|
771
|
+
participants: Object.freeze(denseArray(
|
|
772
|
+
source.participants,
|
|
773
|
+
`${label}.participants`,
|
|
774
|
+
MAX_PARTICIPANTS,
|
|
775
|
+
).map((entry, index) => participant(entry, `${label}.participants[${index}]`))),
|
|
776
|
+
unread: nullableBoolean(source.unread, `${label}.unread`),
|
|
777
|
+
unreadCount: nullableInteger(
|
|
778
|
+
source.unreadCount,
|
|
779
|
+
`${label}.unreadCount`,
|
|
780
|
+
0,
|
|
781
|
+
Number.MAX_SAFE_INTEGER,
|
|
782
|
+
),
|
|
783
|
+
archived: nullableBoolean(source.archived, `${label}.archived`),
|
|
784
|
+
pending: nullableBoolean(source.pending, `${label}.pending`),
|
|
785
|
+
id: common.id,
|
|
786
|
+
source: common.source,
|
|
787
|
+
conversationId: common.conversationId,
|
|
788
|
+
});
|
|
789
|
+
return verifyEntityRevision(semantic, source.revision, label);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function messageEntity(source: JsonRecord, label: string): OmniMessageV1 {
|
|
793
|
+
const hasBodyTruncated = Object.hasOwn(source, "bodyTruncated");
|
|
794
|
+
exactKeys(source, [
|
|
795
|
+
"kind", "providerId", "providerRevision", "orderedAt", "conversationProviderId",
|
|
796
|
+
"sender", "recipients", "direction", "subject", "body", "unread",
|
|
797
|
+
"replyToProviderId", "state", "attachments", "id", "revision", "source",
|
|
798
|
+
"conversationId",
|
|
799
|
+
], hasBodyTruncated ? ["bodyTruncated"] : [], label);
|
|
800
|
+
const common = entityCommon(source, label);
|
|
801
|
+
const conversationProviderId = nullableText(
|
|
802
|
+
source.conversationProviderId,
|
|
803
|
+
`${label}.conversationProviderId`,
|
|
804
|
+
1_024,
|
|
805
|
+
);
|
|
806
|
+
if ((conversationProviderId === null) !== (common.conversationId === null)) {
|
|
807
|
+
return fail(`${label}.conversationId`, "is inconsistent with conversationProviderId");
|
|
808
|
+
}
|
|
809
|
+
const body = nullableText(source.body, `${label}.body`, 256 * 1024, true);
|
|
810
|
+
const bodyTruncated = hasBodyTruncated
|
|
811
|
+
? boolean(source.bodyTruncated, `${label}.bodyTruncated`)
|
|
812
|
+
: undefined;
|
|
813
|
+
if (body === null && bodyTruncated === true) {
|
|
814
|
+
return fail(`${label}.bodyTruncated`, "cannot be true when body is null");
|
|
815
|
+
}
|
|
816
|
+
const semantic = Object.freeze({
|
|
817
|
+
kind: "message" as const,
|
|
818
|
+
providerId: common.providerId,
|
|
819
|
+
providerRevision: common.providerRevision,
|
|
820
|
+
orderedAt: common.orderedAt,
|
|
821
|
+
conversationProviderId,
|
|
822
|
+
sender: source.sender === null ? null : participant(source.sender, `${label}.sender`),
|
|
823
|
+
recipients: Object.freeze(denseArray(
|
|
824
|
+
source.recipients,
|
|
825
|
+
`${label}.recipients`,
|
|
826
|
+
MAX_PARTICIPANTS,
|
|
827
|
+
).map((entry, index) => participant(entry, `${label}.recipients[${index}]`))),
|
|
828
|
+
direction: oneOf(
|
|
829
|
+
source.direction,
|
|
830
|
+
["incoming", "outgoing", "unknown"] as const,
|
|
831
|
+
`${label}.direction`,
|
|
832
|
+
),
|
|
833
|
+
subject: nullableText(source.subject, `${label}.subject`, 8_192, true),
|
|
834
|
+
body,
|
|
835
|
+
...(bodyTruncated === undefined ? {} : { bodyTruncated }),
|
|
836
|
+
unread: nullableBoolean(source.unread, `${label}.unread`),
|
|
837
|
+
replyToProviderId: nullableText(
|
|
838
|
+
source.replyToProviderId,
|
|
839
|
+
`${label}.replyToProviderId`,
|
|
840
|
+
1_024,
|
|
841
|
+
),
|
|
842
|
+
state: oneOf(
|
|
843
|
+
source.state,
|
|
844
|
+
["active", "revoked", "deleted-for-me", "revoked-and-deleted-for-me"] as const,
|
|
845
|
+
`${label}.state`,
|
|
846
|
+
),
|
|
847
|
+
attachments: Object.freeze(denseArray(
|
|
848
|
+
source.attachments,
|
|
849
|
+
`${label}.attachments`,
|
|
850
|
+
MAX_ATTACHMENTS,
|
|
851
|
+
).map((entry, index) => attachment(entry, `${label}.attachments[${index}]`))),
|
|
852
|
+
id: common.id,
|
|
853
|
+
source: common.source,
|
|
854
|
+
conversationId: common.conversationId,
|
|
855
|
+
});
|
|
856
|
+
return verifyEntityRevision(semantic, source.revision, label);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function notificationEntity(
|
|
860
|
+
source: JsonRecord,
|
|
861
|
+
label: string,
|
|
862
|
+
): OmniNotificationV1 {
|
|
863
|
+
exactKeys(source, [
|
|
864
|
+
"kind", "providerId", "providerRevision", "orderedAt", "actor", "subject",
|
|
865
|
+
"body", "unread", "context", "id", "revision", "source", "conversationId",
|
|
866
|
+
], [], label);
|
|
867
|
+
const common = entityCommon(source, label);
|
|
868
|
+
if (common.conversationId !== null) {
|
|
869
|
+
return fail(`${label}.conversationId`, "must be null for notifications");
|
|
870
|
+
}
|
|
871
|
+
const semantic = Object.freeze({
|
|
872
|
+
kind: "notification" as const,
|
|
873
|
+
providerId: common.providerId,
|
|
874
|
+
providerRevision: common.providerRevision,
|
|
875
|
+
orderedAt: common.orderedAt,
|
|
876
|
+
actor: source.actor === null ? null : participant(source.actor, `${label}.actor`),
|
|
877
|
+
subject: nullableText(source.subject, `${label}.subject`, 8_192, true),
|
|
878
|
+
body: nullableText(source.body, `${label}.body`, 256 * 1024, true),
|
|
879
|
+
unread: nullableBoolean(source.unread, `${label}.unread`),
|
|
880
|
+
context: nullableText(source.context, `${label}.context`, 8_192),
|
|
881
|
+
id: common.id,
|
|
882
|
+
source: common.source,
|
|
883
|
+
conversationId: common.conversationId,
|
|
884
|
+
});
|
|
885
|
+
return verifyEntityRevision(semantic, source.revision, label);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function entity(value: unknown, index: number): OmniEntityV1 {
|
|
889
|
+
const label = `Ghostget omni view.entities[${index}]`;
|
|
890
|
+
const source = record(value, label);
|
|
891
|
+
const kind = oneOf(
|
|
892
|
+
source.kind,
|
|
893
|
+
["conversation", "message", "notification"] as const,
|
|
894
|
+
`${label}.kind`,
|
|
895
|
+
);
|
|
896
|
+
if (kind === "conversation") return conversationEntity(source, label);
|
|
897
|
+
if (kind === "message") return messageEntity(source, label);
|
|
898
|
+
return notificationEntity(source, label);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function freshness(value: unknown, label: string): OmniExactFreshnessV1 {
|
|
902
|
+
const source = record(value, label);
|
|
903
|
+
exactKeys(source, ["state", "freshForMs"], [], label);
|
|
904
|
+
const state = oneOf(
|
|
905
|
+
source.state,
|
|
906
|
+
["fresh", "stale", "unclassified"] as const,
|
|
907
|
+
`${label}.state`,
|
|
908
|
+
);
|
|
909
|
+
const freshForMs = source.freshForMs === null
|
|
910
|
+
? null
|
|
911
|
+
: integer(source.freshForMs, `${label}.freshForMs`, 0, 365 * 24 * 60 * 60 * 1_000);
|
|
912
|
+
if ((state === "unclassified") !== (freshForMs === null)) {
|
|
913
|
+
return fail(label, "has inconsistent classification and window");
|
|
914
|
+
}
|
|
915
|
+
return Object.freeze({ state, freshForMs });
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function coverage(value: unknown, label: string): OmniCoverageSourceStatusV1 {
|
|
919
|
+
const source = record(value, label);
|
|
920
|
+
const state = oneOf(
|
|
921
|
+
source.state,
|
|
922
|
+
["unavailable", "observed"] as const,
|
|
923
|
+
`${label}.state`,
|
|
924
|
+
);
|
|
925
|
+
if (state === "unavailable") {
|
|
926
|
+
exactKeys(source, ["state", "reason"], [], label);
|
|
927
|
+
return Object.freeze({
|
|
928
|
+
state,
|
|
929
|
+
reason: text(source.reason, `${label}.reason`, 8_192),
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
exactKeys(source, ["state", "kind", "continuation", "reason"], [], label);
|
|
933
|
+
const kind = oneOf(
|
|
934
|
+
source.kind,
|
|
935
|
+
[
|
|
936
|
+
"complete",
|
|
937
|
+
"page",
|
|
938
|
+
"unknown",
|
|
939
|
+
"first-page-only",
|
|
940
|
+
"bounded-local",
|
|
941
|
+
"search-window",
|
|
942
|
+
"truncated",
|
|
943
|
+
] as const,
|
|
944
|
+
`${label}.kind`,
|
|
945
|
+
);
|
|
946
|
+
const continuation = oneOf(
|
|
947
|
+
source.continuation,
|
|
948
|
+
["none", "pending", "unavailable"] as const,
|
|
949
|
+
`${label}.continuation`,
|
|
950
|
+
);
|
|
951
|
+
if (kind === "complete" && continuation !== "none") {
|
|
952
|
+
return fail(label, "must not continue an authoritative complete set");
|
|
953
|
+
}
|
|
954
|
+
if (kind === "first-page-only" && continuation !== "unavailable") {
|
|
955
|
+
return fail(label, "must mark first-page-only continuation unavailable");
|
|
956
|
+
}
|
|
957
|
+
return Object.freeze({
|
|
958
|
+
state,
|
|
959
|
+
kind,
|
|
960
|
+
continuation,
|
|
961
|
+
reason: nullableText(source.reason, `${label}.reason`, 8_192, true),
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function sourceStatus(value: unknown, index: number): OmniViewSourceStatusV1 {
|
|
966
|
+
const label = `Ghostget omni view.sources[${index}]`;
|
|
967
|
+
const source = record(value, label);
|
|
968
|
+
exactKeys(source, [
|
|
969
|
+
"adapterId", "operationId", "authId", "requestInputHash",
|
|
970
|
+
"projectionInputHash", "normalizationDataRevision", "surfaceId", "exact",
|
|
971
|
+
"normalization", "coverage",
|
|
972
|
+
], [], label);
|
|
973
|
+
const exact = record(source.exact, `${label}.exact`);
|
|
974
|
+
exactKeys(
|
|
975
|
+
exact,
|
|
976
|
+
["state", "key"],
|
|
977
|
+
["dataRevision", "validatedAt", "ageMs", "freshness", "reason"],
|
|
978
|
+
`${label}.exact`,
|
|
979
|
+
);
|
|
980
|
+
const exactState = oneOf(
|
|
981
|
+
exact.state,
|
|
982
|
+
["hit", "miss", "error"] as const,
|
|
983
|
+
`${label}.exact.state`,
|
|
984
|
+
);
|
|
985
|
+
const dataRevision = exact.dataRevision === undefined
|
|
986
|
+
? undefined
|
|
987
|
+
: digest(exact.dataRevision, `${label}.exact.dataRevision`);
|
|
988
|
+
const validatedAt = exact.validatedAt === undefined
|
|
989
|
+
? undefined
|
|
990
|
+
: timestamp(exact.validatedAt, `${label}.exact.validatedAt`);
|
|
991
|
+
const ageMs = exact.ageMs === undefined
|
|
992
|
+
? undefined
|
|
993
|
+
: integer(exact.ageMs, `${label}.exact.ageMs`, 0, Number.MAX_SAFE_INTEGER);
|
|
994
|
+
const parsedFreshness = exact.freshness === undefined
|
|
995
|
+
? undefined
|
|
996
|
+
: freshness(exact.freshness, `${label}.exact.freshness`);
|
|
997
|
+
const exactReason = exact.reason === undefined
|
|
998
|
+
? undefined
|
|
999
|
+
: text(exact.reason, `${label}.exact.reason`, 8_192);
|
|
1000
|
+
if (
|
|
1001
|
+
exactState === "hit"
|
|
1002
|
+
&& (
|
|
1003
|
+
dataRevision === undefined
|
|
1004
|
+
|| validatedAt === undefined
|
|
1005
|
+
|| ageMs === undefined
|
|
1006
|
+
|| parsedFreshness === undefined
|
|
1007
|
+
|| exactReason !== undefined
|
|
1008
|
+
)
|
|
1009
|
+
) return fail(`${label}.exact`, "must fully describe a hit");
|
|
1010
|
+
if (
|
|
1011
|
+
exactState === "miss"
|
|
1012
|
+
&& (
|
|
1013
|
+
dataRevision !== undefined
|
|
1014
|
+
|| validatedAt !== undefined
|
|
1015
|
+
|| ageMs !== undefined
|
|
1016
|
+
|| parsedFreshness !== undefined
|
|
1017
|
+
|| exactReason !== undefined
|
|
1018
|
+
)
|
|
1019
|
+
) return fail(`${label}.exact`, "must not attach metadata to a miss");
|
|
1020
|
+
if (
|
|
1021
|
+
exactState === "error"
|
|
1022
|
+
&& (
|
|
1023
|
+
dataRevision !== undefined
|
|
1024
|
+
|| validatedAt !== undefined
|
|
1025
|
+
|| ageMs !== undefined
|
|
1026
|
+
|| parsedFreshness !== undefined
|
|
1027
|
+
|| exactReason === undefined
|
|
1028
|
+
)
|
|
1029
|
+
) return fail(`${label}.exact`, "must describe only the exact read error");
|
|
1030
|
+
if (
|
|
1031
|
+
exactState === "hit"
|
|
1032
|
+
&& parsedFreshness!.state !== "unclassified"
|
|
1033
|
+
&& (
|
|
1034
|
+
(parsedFreshness!.state === "fresh")
|
|
1035
|
+
!== (ageMs! <= parsedFreshness!.freshForMs!)
|
|
1036
|
+
)
|
|
1037
|
+
) {
|
|
1038
|
+
return fail(
|
|
1039
|
+
`${label}.exact.freshness`,
|
|
1040
|
+
"does not match the returned cache age",
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
const exactKey = digest(exact.key, `${label}.exact.key`);
|
|
1044
|
+
const exactStatus: OmniExactSourceStatusV1 = exactState === "hit"
|
|
1045
|
+
? Object.freeze({
|
|
1046
|
+
state: exactState,
|
|
1047
|
+
key: exactKey,
|
|
1048
|
+
dataRevision: dataRevision!,
|
|
1049
|
+
validatedAt: validatedAt!,
|
|
1050
|
+
ageMs: ageMs!,
|
|
1051
|
+
freshness: parsedFreshness!,
|
|
1052
|
+
})
|
|
1053
|
+
: exactState === "error"
|
|
1054
|
+
? Object.freeze({ state: exactState, key: exactKey, reason: exactReason! })
|
|
1055
|
+
: Object.freeze({ state: exactState, key: exactKey });
|
|
1056
|
+
|
|
1057
|
+
const normalization = record(source.normalization, `${label}.normalization`);
|
|
1058
|
+
exactKeys(
|
|
1059
|
+
normalization,
|
|
1060
|
+
["state"],
|
|
1061
|
+
[
|
|
1062
|
+
"reason", "exactQueryKey", "exactDataRevision",
|
|
1063
|
+
"normalizedExactDataRevision", "failedExactDataRevision",
|
|
1064
|
+
"newerExactDataRevision", "lastGoodExactDataRevision", "lastGoodAt",
|
|
1065
|
+
],
|
|
1066
|
+
`${label}.normalization`,
|
|
1067
|
+
);
|
|
1068
|
+
const normalizationState = oneOf(
|
|
1069
|
+
normalization.state,
|
|
1070
|
+
[
|
|
1071
|
+
"current",
|
|
1072
|
+
"missing",
|
|
1073
|
+
"unsupported",
|
|
1074
|
+
"retained-after-drift",
|
|
1075
|
+
"stale",
|
|
1076
|
+
"error",
|
|
1077
|
+
] as const,
|
|
1078
|
+
`${label}.normalization.state`,
|
|
1079
|
+
);
|
|
1080
|
+
const reason = normalization.reason === undefined
|
|
1081
|
+
? undefined
|
|
1082
|
+
: text(normalization.reason, `${label}.normalization.reason`, 8_192);
|
|
1083
|
+
const exactQueryKey = normalization.exactQueryKey === undefined
|
|
1084
|
+
? undefined
|
|
1085
|
+
: digest(
|
|
1086
|
+
normalization.exactQueryKey,
|
|
1087
|
+
`${label}.normalization.exactQueryKey`,
|
|
1088
|
+
);
|
|
1089
|
+
const exactDataRevision = normalization.exactDataRevision === undefined
|
|
1090
|
+
? undefined
|
|
1091
|
+
: normalization.exactDataRevision === null
|
|
1092
|
+
? null
|
|
1093
|
+
: digest(
|
|
1094
|
+
normalization.exactDataRevision,
|
|
1095
|
+
`${label}.normalization.exactDataRevision`,
|
|
1096
|
+
);
|
|
1097
|
+
const normalizedExactDataRevision = normalization.normalizedExactDataRevision === undefined
|
|
1098
|
+
? undefined
|
|
1099
|
+
: normalization.normalizedExactDataRevision === null
|
|
1100
|
+
? null
|
|
1101
|
+
: digest(
|
|
1102
|
+
normalization.normalizedExactDataRevision,
|
|
1103
|
+
`${label}.normalization.normalizedExactDataRevision`,
|
|
1104
|
+
);
|
|
1105
|
+
const failedExactDataRevision = normalization.failedExactDataRevision === undefined
|
|
1106
|
+
? undefined
|
|
1107
|
+
: digest(
|
|
1108
|
+
normalization.failedExactDataRevision,
|
|
1109
|
+
`${label}.normalization.failedExactDataRevision`,
|
|
1110
|
+
);
|
|
1111
|
+
const newerExactDataRevision = normalization.newerExactDataRevision === undefined
|
|
1112
|
+
? undefined
|
|
1113
|
+
: normalization.newerExactDataRevision === null
|
|
1114
|
+
? null
|
|
1115
|
+
: digest(
|
|
1116
|
+
normalization.newerExactDataRevision,
|
|
1117
|
+
`${label}.normalization.newerExactDataRevision`,
|
|
1118
|
+
);
|
|
1119
|
+
const lastGoodExactDataRevision = normalization.lastGoodExactDataRevision
|
|
1120
|
+
=== undefined
|
|
1121
|
+
? undefined
|
|
1122
|
+
: normalization.lastGoodExactDataRevision === null
|
|
1123
|
+
? null
|
|
1124
|
+
: digest(
|
|
1125
|
+
normalization.lastGoodExactDataRevision,
|
|
1126
|
+
`${label}.normalization.lastGoodExactDataRevision`,
|
|
1127
|
+
);
|
|
1128
|
+
const lastGoodAt = normalization.lastGoodAt === undefined
|
|
1129
|
+
? undefined
|
|
1130
|
+
: nullableTimestamp(
|
|
1131
|
+
normalization.lastGoodAt,
|
|
1132
|
+
`${label}.normalization.lastGoodAt`,
|
|
1133
|
+
);
|
|
1134
|
+
let normalizationStatus: OmniNormalizationSourceStatusV1;
|
|
1135
|
+
if (normalizationState === "missing") {
|
|
1136
|
+
if (
|
|
1137
|
+
reason !== undefined
|
|
1138
|
+
|| exactQueryKey !== undefined
|
|
1139
|
+
|| exactDataRevision !== undefined
|
|
1140
|
+
|| normalizedExactDataRevision !== undefined
|
|
1141
|
+
|| failedExactDataRevision !== undefined
|
|
1142
|
+
|| newerExactDataRevision !== undefined
|
|
1143
|
+
|| lastGoodExactDataRevision !== undefined
|
|
1144
|
+
|| lastGoodAt !== undefined
|
|
1145
|
+
) return fail(`${label}.normalization`, "must not attach metadata to a miss");
|
|
1146
|
+
normalizationStatus = Object.freeze({ state: normalizationState });
|
|
1147
|
+
} else if (normalizationState === "unsupported") {
|
|
1148
|
+
if (
|
|
1149
|
+
reason === undefined
|
|
1150
|
+
|| exactQueryKey !== undefined
|
|
1151
|
+
|| exactDataRevision !== undefined
|
|
1152
|
+
|| normalizedExactDataRevision !== undefined
|
|
1153
|
+
|| failedExactDataRevision !== undefined
|
|
1154
|
+
|| newerExactDataRevision !== undefined
|
|
1155
|
+
|| lastGoodExactDataRevision !== undefined
|
|
1156
|
+
|| lastGoodAt !== undefined
|
|
1157
|
+
) return fail(`${label}.normalization`, "must describe only why it is unsupported");
|
|
1158
|
+
normalizationStatus = Object.freeze({ state: normalizationState, reason });
|
|
1159
|
+
} else if (normalizationState === "current") {
|
|
1160
|
+
if (
|
|
1161
|
+
reason !== undefined
|
|
1162
|
+
|| exactQueryKey === undefined
|
|
1163
|
+
|| exactDataRevision === undefined
|
|
1164
|
+
|| exactDataRevision === null
|
|
1165
|
+
|| normalizedExactDataRevision !== undefined
|
|
1166
|
+
|| failedExactDataRevision !== undefined
|
|
1167
|
+
|| newerExactDataRevision !== undefined
|
|
1168
|
+
|| lastGoodExactDataRevision !== undefined
|
|
1169
|
+
|| lastGoodAt === undefined
|
|
1170
|
+
|| lastGoodAt === null
|
|
1171
|
+
) return fail(`${label}.normalization`, "must fully describe current normalized state");
|
|
1172
|
+
normalizationStatus = Object.freeze({
|
|
1173
|
+
state: normalizationState,
|
|
1174
|
+
exactQueryKey,
|
|
1175
|
+
exactDataRevision,
|
|
1176
|
+
lastGoodAt,
|
|
1177
|
+
});
|
|
1178
|
+
} else if (normalizationState === "retained-after-drift") {
|
|
1179
|
+
if (
|
|
1180
|
+
reason === undefined
|
|
1181
|
+
|| exactQueryKey === undefined
|
|
1182
|
+
|| exactDataRevision !== undefined
|
|
1183
|
+
|| normalizedExactDataRevision !== undefined
|
|
1184
|
+
|| failedExactDataRevision === undefined
|
|
1185
|
+
|| newerExactDataRevision === undefined
|
|
1186
|
+
|| lastGoodExactDataRevision === undefined
|
|
1187
|
+
|| lastGoodAt === undefined
|
|
1188
|
+
) return fail(`${label}.normalization`, "must fully describe retained drift state");
|
|
1189
|
+
if (
|
|
1190
|
+
(lastGoodExactDataRevision === null) !== (lastGoodAt === null)
|
|
1191
|
+
) {
|
|
1192
|
+
return fail(
|
|
1193
|
+
`${label}.normalization`,
|
|
1194
|
+
"must pair last-good revision and observation time",
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
if (
|
|
1198
|
+
newerExactDataRevision !== null
|
|
1199
|
+
&& newerExactDataRevision === failedExactDataRevision
|
|
1200
|
+
) {
|
|
1201
|
+
return fail(
|
|
1202
|
+
`${label}.normalization.newerExactDataRevision`,
|
|
1203
|
+
"must differ from the failed exact revision",
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
normalizationStatus = Object.freeze({
|
|
1207
|
+
state: normalizationState,
|
|
1208
|
+
exactQueryKey,
|
|
1209
|
+
reason,
|
|
1210
|
+
failedExactDataRevision,
|
|
1211
|
+
newerExactDataRevision,
|
|
1212
|
+
lastGoodExactDataRevision,
|
|
1213
|
+
lastGoodAt,
|
|
1214
|
+
});
|
|
1215
|
+
} else if (normalizationState === "stale") {
|
|
1216
|
+
if (
|
|
1217
|
+
reason === undefined
|
|
1218
|
+
|| exactQueryKey === undefined
|
|
1219
|
+
|| exactDataRevision === undefined
|
|
1220
|
+
|| normalizedExactDataRevision === undefined
|
|
1221
|
+
|| failedExactDataRevision !== undefined
|
|
1222
|
+
|| newerExactDataRevision !== undefined
|
|
1223
|
+
|| lastGoodExactDataRevision !== undefined
|
|
1224
|
+
|| lastGoodAt === undefined
|
|
1225
|
+
) return fail(`${label}.normalization`, "must fully describe stale normalized state");
|
|
1226
|
+
if (
|
|
1227
|
+
(normalizedExactDataRevision === null) !== (lastGoodAt === null)
|
|
1228
|
+
) {
|
|
1229
|
+
return fail(
|
|
1230
|
+
`${label}.normalization`,
|
|
1231
|
+
"must pair normalized revision and observation time",
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
normalizationStatus = Object.freeze({
|
|
1235
|
+
state: normalizationState,
|
|
1236
|
+
exactQueryKey,
|
|
1237
|
+
exactDataRevision,
|
|
1238
|
+
normalizedExactDataRevision,
|
|
1239
|
+
lastGoodAt,
|
|
1240
|
+
reason,
|
|
1241
|
+
});
|
|
1242
|
+
} else {
|
|
1243
|
+
if (
|
|
1244
|
+
reason === undefined
|
|
1245
|
+
|| exactQueryKey !== undefined
|
|
1246
|
+
|| exactDataRevision !== undefined
|
|
1247
|
+
|| normalizedExactDataRevision !== undefined
|
|
1248
|
+
|| failedExactDataRevision !== undefined
|
|
1249
|
+
|| newerExactDataRevision !== undefined
|
|
1250
|
+
|| lastGoodExactDataRevision !== undefined
|
|
1251
|
+
|| lastGoodAt === undefined
|
|
1252
|
+
) return fail(`${label}.normalization`, "must describe only the normalization error");
|
|
1253
|
+
normalizationStatus = Object.freeze({
|
|
1254
|
+
state: normalizationState,
|
|
1255
|
+
reason,
|
|
1256
|
+
lastGoodAt,
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
if (
|
|
1260
|
+
"exactQueryKey" in normalizationStatus
|
|
1261
|
+
&& normalizationStatus.exactQueryKey === exactStatus.key
|
|
1262
|
+
) {
|
|
1263
|
+
let normalizationRevision: string | null;
|
|
1264
|
+
let normalizationRevisionField:
|
|
1265
|
+
| "exactDataRevision"
|
|
1266
|
+
| "normalizedExactDataRevision"
|
|
1267
|
+
| "failedExactDataRevision"
|
|
1268
|
+
| "newerExactDataRevision";
|
|
1269
|
+
if (normalizationStatus.state === "current") {
|
|
1270
|
+
normalizationRevision = normalizationStatus.exactDataRevision;
|
|
1271
|
+
normalizationRevisionField = "exactDataRevision";
|
|
1272
|
+
} else if (normalizationStatus.state === "retained-after-drift") {
|
|
1273
|
+
normalizationRevision = normalizationStatus.newerExactDataRevision
|
|
1274
|
+
?? normalizationStatus.failedExactDataRevision;
|
|
1275
|
+
normalizationRevisionField = normalizationStatus.newerExactDataRevision === null
|
|
1276
|
+
? "failedExactDataRevision"
|
|
1277
|
+
: "newerExactDataRevision";
|
|
1278
|
+
} else if (normalizationStatus.exactDataRevision !== null) {
|
|
1279
|
+
normalizationRevision = normalizationStatus.exactDataRevision;
|
|
1280
|
+
normalizationRevisionField = "exactDataRevision";
|
|
1281
|
+
} else {
|
|
1282
|
+
normalizationRevision = normalizationStatus.normalizedExactDataRevision;
|
|
1283
|
+
normalizationRevisionField = "normalizedExactDataRevision";
|
|
1284
|
+
}
|
|
1285
|
+
if (
|
|
1286
|
+
exactStatus.state === "hit"
|
|
1287
|
+
&& exactStatus.dataRevision !== normalizationRevision
|
|
1288
|
+
) {
|
|
1289
|
+
return fail(
|
|
1290
|
+
`${label}.normalization.${normalizationRevisionField}`,
|
|
1291
|
+
"does not match the exact hit",
|
|
1292
|
+
);
|
|
1293
|
+
}
|
|
1294
|
+
if (exactStatus.state !== "hit" && normalizationStatus.state === "current") {
|
|
1295
|
+
return fail(`${label}.normalization`, "cannot be current without its exact hit");
|
|
1296
|
+
}
|
|
1297
|
+
if (
|
|
1298
|
+
exactStatus.state !== "hit"
|
|
1299
|
+
&& normalizationStatus.state === "retained-after-drift"
|
|
1300
|
+
&& normalizationStatus.newerExactDataRevision !== null
|
|
1301
|
+
) {
|
|
1302
|
+
return fail(
|
|
1303
|
+
`${label}.normalization.newerExactDataRevision`,
|
|
1304
|
+
"cannot name a newer current revision without an exact hit",
|
|
1305
|
+
);
|
|
1306
|
+
}
|
|
1307
|
+
if (
|
|
1308
|
+
exactStatus.state !== "hit"
|
|
1309
|
+
&& normalizationStatus.state === "stale"
|
|
1310
|
+
&& normalizationStatus.exactDataRevision !== null
|
|
1311
|
+
) {
|
|
1312
|
+
return fail(
|
|
1313
|
+
`${label}.normalization.exactDataRevision`,
|
|
1314
|
+
"cannot name a current revision without an exact hit",
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
if (
|
|
1319
|
+
normalizationStatus.state === "current"
|
|
1320
|
+
&& normalizationStatus.exactQueryKey !== exactStatus.key
|
|
1321
|
+
) {
|
|
1322
|
+
return fail(
|
|
1323
|
+
`${label}.normalization.exactQueryKey`,
|
|
1324
|
+
"must name the root exact query while normalization is current",
|
|
1325
|
+
);
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
const normalizationDataRevision = source.normalizationDataRevision === null
|
|
1329
|
+
? null
|
|
1330
|
+
: digest(
|
|
1331
|
+
source.normalizationDataRevision,
|
|
1332
|
+
`${label}.normalizationDataRevision`,
|
|
1333
|
+
);
|
|
1334
|
+
if (
|
|
1335
|
+
(
|
|
1336
|
+
normalizationStatus.state === "current"
|
|
1337
|
+
|| normalizationStatus.state === "stale"
|
|
1338
|
+
|| normalizationStatus.state === "retained-after-drift"
|
|
1339
|
+
)
|
|
1340
|
+
&& normalizationDataRevision === null
|
|
1341
|
+
) {
|
|
1342
|
+
return fail(
|
|
1343
|
+
`${label}.normalizationDataRevision`,
|
|
1344
|
+
`is required while normalization is ${normalizationStatus.state}`,
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
if (
|
|
1348
|
+
normalizationStatus.state === "unsupported"
|
|
1349
|
+
&& normalizationDataRevision !== null
|
|
1350
|
+
) {
|
|
1351
|
+
return fail(
|
|
1352
|
+
`${label}.normalizationDataRevision`,
|
|
1353
|
+
"must be null while normalization is unsupported",
|
|
1354
|
+
);
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
const parsedCoverage = coverage(source.coverage, `${label}.coverage`);
|
|
1358
|
+
if (
|
|
1359
|
+
normalizationStatus.state === "current"
|
|
1360
|
+
&& parsedCoverage.state === "unavailable"
|
|
1361
|
+
) {
|
|
1362
|
+
return fail(
|
|
1363
|
+
`${label}.coverage`,
|
|
1364
|
+
"cannot be unavailable for current normalized state",
|
|
1365
|
+
);
|
|
1366
|
+
}
|
|
1367
|
+
if (
|
|
1368
|
+
normalizationStatus.state === "current"
|
|
1369
|
+
&& parsedCoverage.state === "observed"
|
|
1370
|
+
&& parsedCoverage.continuation === "pending"
|
|
1371
|
+
) {
|
|
1372
|
+
return fail(
|
|
1373
|
+
`${label}.coverage.continuation`,
|
|
1374
|
+
"cannot be pending for current normalized state",
|
|
1375
|
+
);
|
|
1376
|
+
}
|
|
1377
|
+
if (
|
|
1378
|
+
(
|
|
1379
|
+
normalizationStatus.state === "missing"
|
|
1380
|
+
|| normalizationStatus.state === "unsupported"
|
|
1381
|
+
|| normalizationStatus.state === "error"
|
|
1382
|
+
)
|
|
1383
|
+
&& parsedCoverage.state === "observed"
|
|
1384
|
+
) {
|
|
1385
|
+
return fail(
|
|
1386
|
+
`${label}.coverage`,
|
|
1387
|
+
`cannot be observed while normalization is ${normalizationStatus.state}`,
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
return Object.freeze({
|
|
1392
|
+
adapterId: text(source.adapterId, `${label}.adapterId`, 64),
|
|
1393
|
+
operationId: oneOf(
|
|
1394
|
+
source.operationId,
|
|
1395
|
+
["messaging.list", "messaging.read"] as const,
|
|
1396
|
+
`${label}.operationId`,
|
|
1397
|
+
),
|
|
1398
|
+
authId: text(source.authId, `${label}.authId`, 64),
|
|
1399
|
+
requestInputHash: digest(
|
|
1400
|
+
source.requestInputHash,
|
|
1401
|
+
`${label}.requestInputHash`,
|
|
1402
|
+
),
|
|
1403
|
+
projectionInputHash: digest(
|
|
1404
|
+
source.projectionInputHash,
|
|
1405
|
+
`${label}.projectionInputHash`,
|
|
1406
|
+
),
|
|
1407
|
+
normalizationDataRevision,
|
|
1408
|
+
surfaceId: text(source.surfaceId, `${label}.surfaceId`, 64),
|
|
1409
|
+
exact: exactStatus,
|
|
1410
|
+
normalization: normalizationStatus,
|
|
1411
|
+
coverage: parsedCoverage,
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
function sourceCoordinateKey(value: {
|
|
1416
|
+
readonly adapterId: string;
|
|
1417
|
+
readonly operationId: "messaging.list" | "messaging.read";
|
|
1418
|
+
readonly authId: string;
|
|
1419
|
+
readonly requestInputHash: string;
|
|
1420
|
+
}): string {
|
|
1421
|
+
return canonicalJson({
|
|
1422
|
+
adapterId: value.adapterId,
|
|
1423
|
+
operationId: value.operationId,
|
|
1424
|
+
authId: value.authId,
|
|
1425
|
+
requestInputHash: value.requestInputHash,
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
function compareEntities(left: OmniEntityV1, right: OmniEntityV1): number {
|
|
1430
|
+
const leftTime = left.orderedAt ?? "";
|
|
1431
|
+
const rightTime = right.orderedAt ?? "";
|
|
1432
|
+
return rightTime.localeCompare(leftTime) || left.id.localeCompare(right.id);
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
function view(value: unknown, request: PreparedRequest): OmniViewV1 {
|
|
1436
|
+
const source = record(value, "Ghostget omni view");
|
|
1437
|
+
exactKeys(
|
|
1438
|
+
source,
|
|
1439
|
+
["schemaVersion", "viewRevision", "entities", "nextCursor", "sources"],
|
|
1440
|
+
[],
|
|
1441
|
+
"Ghostget omni view",
|
|
1442
|
+
);
|
|
1443
|
+
if (source.schemaVersion !== 1) return fail("Ghostget omni view.schemaVersion", "must be 1");
|
|
1444
|
+
const entities = Object.freeze(denseArray(
|
|
1445
|
+
source.entities,
|
|
1446
|
+
"Ghostget omni view.entities",
|
|
1447
|
+
MAX_VIEW_ENTITIES,
|
|
1448
|
+
).map(entity));
|
|
1449
|
+
if (new Set(entities.map((entry) => entry.id)).size !== entities.length) {
|
|
1450
|
+
return fail("Ghostget omni view.entities", "must not repeat entity identities");
|
|
1451
|
+
}
|
|
1452
|
+
for (let index = 1; index < entities.length; index += 1) {
|
|
1453
|
+
const prior = entities[index - 1];
|
|
1454
|
+
const current = entities[index];
|
|
1455
|
+
if (prior !== undefined && current !== undefined && compareEntities(prior, current) > 0) {
|
|
1456
|
+
return fail("Ghostget omni view.entities", "must use deterministic newest-first order");
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
if (entities.length > request.pageLimit) {
|
|
1460
|
+
return fail("Ghostget omni view.entities", "exceeds the requested page limit");
|
|
1461
|
+
}
|
|
1462
|
+
for (const entry of entities) {
|
|
1463
|
+
if (
|
|
1464
|
+
request.filter.kinds !== null
|
|
1465
|
+
&& !request.filter.kinds.includes(entry.kind)
|
|
1466
|
+
) {
|
|
1467
|
+
return fail("Ghostget omni view.entities", "contains a kind excluded by the request");
|
|
1468
|
+
}
|
|
1469
|
+
if (
|
|
1470
|
+
request.filter.conversationId !== null
|
|
1471
|
+
&& entry.conversationId !== request.filter.conversationId
|
|
1472
|
+
) {
|
|
1473
|
+
return fail(
|
|
1474
|
+
"Ghostget omni view.entities",
|
|
1475
|
+
"contains an entity outside the requested conversation",
|
|
1476
|
+
);
|
|
1477
|
+
}
|
|
1478
|
+
if (request.filter.unread !== null) {
|
|
1479
|
+
const unread = entry.kind === "conversation"
|
|
1480
|
+
? entry.unread ?? (entry.unreadCount === null ? null : entry.unreadCount > 0)
|
|
1481
|
+
: entry.unread;
|
|
1482
|
+
if (unread !== request.filter.unread) {
|
|
1483
|
+
return fail(
|
|
1484
|
+
"Ghostget omni view.entities",
|
|
1485
|
+
"contains an entity outside the requested unread state",
|
|
1486
|
+
);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
const nextCursor = source.nextCursor === null
|
|
1491
|
+
? null
|
|
1492
|
+
: localCursor(
|
|
1493
|
+
source.nextCursor,
|
|
1494
|
+
"Ghostget omni view.nextCursor",
|
|
1495
|
+
);
|
|
1496
|
+
if (nextCursor !== null && entities.length !== request.pageLimit) {
|
|
1497
|
+
return fail(
|
|
1498
|
+
"Ghostget omni view.nextCursor",
|
|
1499
|
+
"requires a full requested page",
|
|
1500
|
+
);
|
|
1501
|
+
}
|
|
1502
|
+
const sources = Object.freeze(denseArray(
|
|
1503
|
+
source.sources,
|
|
1504
|
+
"Ghostget omni view.sources",
|
|
1505
|
+
MAX_SOURCES,
|
|
1506
|
+
).map(sourceStatus));
|
|
1507
|
+
if (sources.length === 0) return fail("Ghostget omni view.sources", "must not be empty");
|
|
1508
|
+
const sourceKeys = sources.map((entry) => canonicalJson({
|
|
1509
|
+
adapterId: entry.adapterId,
|
|
1510
|
+
operationId: entry.operationId,
|
|
1511
|
+
authId: entry.authId,
|
|
1512
|
+
requestInputHash: entry.requestInputHash,
|
|
1513
|
+
}));
|
|
1514
|
+
if (new Set(sourceKeys).size !== sourceKeys.length) {
|
|
1515
|
+
return fail("Ghostget omni view.sources", "must not repeat source identities");
|
|
1516
|
+
}
|
|
1517
|
+
const expectedSourceKeys = request.sources.map(sourceCoordinateKey);
|
|
1518
|
+
const actualSourceKeys = sources.map(sourceCoordinateKey);
|
|
1519
|
+
if (
|
|
1520
|
+
expectedSourceKeys.length !== actualSourceKeys.length
|
|
1521
|
+
|| expectedSourceKeys.some((key, index) => key !== actualSourceKeys[index])
|
|
1522
|
+
) {
|
|
1523
|
+
return fail(
|
|
1524
|
+
"Ghostget omni view.sources",
|
|
1525
|
+
"must exactly match the requested source coordinates and order",
|
|
1526
|
+
);
|
|
1527
|
+
}
|
|
1528
|
+
const returnedEntitySources = new Set(sources.map((entry) =>
|
|
1529
|
+
canonicalJson({ authId: entry.authId, surfaceId: entry.surfaceId })));
|
|
1530
|
+
for (const entry of entities) {
|
|
1531
|
+
const sourceKey = canonicalJson({
|
|
1532
|
+
authId: entry.source.authId,
|
|
1533
|
+
surfaceId: entry.source.surfaceId,
|
|
1534
|
+
});
|
|
1535
|
+
if (!returnedEntitySources.has(sourceKey)) {
|
|
1536
|
+
return fail(
|
|
1537
|
+
"Ghostget omni view.entities",
|
|
1538
|
+
"must belong to one returned requested source",
|
|
1539
|
+
);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
return Object.freeze({
|
|
1543
|
+
schemaVersion: 1,
|
|
1544
|
+
viewRevision: digest(source.viewRevision, "Ghostget omni view.viewRevision"),
|
|
1545
|
+
entities,
|
|
1546
|
+
nextCursor,
|
|
1547
|
+
sources,
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
function identity(value: unknown): OmniViewIdentity {
|
|
1552
|
+
const source = record(value, "Ghostget omni response identity");
|
|
1553
|
+
exactKeys(
|
|
1554
|
+
source,
|
|
1555
|
+
["invocationDigest", "requestDigest", "sourceSetDigest"],
|
|
1556
|
+
[],
|
|
1557
|
+
"Ghostget omni response identity",
|
|
1558
|
+
);
|
|
1559
|
+
return Object.freeze({
|
|
1560
|
+
invocationDigest: digest(
|
|
1561
|
+
source.invocationDigest,
|
|
1562
|
+
"Ghostget omni response invocation digest",
|
|
1563
|
+
),
|
|
1564
|
+
requestDigest: digest(
|
|
1565
|
+
source.requestDigest,
|
|
1566
|
+
"Ghostget omni response request digest",
|
|
1567
|
+
),
|
|
1568
|
+
sourceSetDigest: digest(
|
|
1569
|
+
source.sourceSetDigest,
|
|
1570
|
+
"Ghostget omni response source-set digest",
|
|
1571
|
+
),
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
function envelope(value: unknown, request: PreparedRequest): ParsedEnvelope {
|
|
1576
|
+
const source = record(value, "Ghostget omni response");
|
|
1577
|
+
exactKeys(
|
|
1578
|
+
source,
|
|
1579
|
+
["ok", "schemaVersion", "source", "identity", "view"],
|
|
1580
|
+
[],
|
|
1581
|
+
"Ghostget omni response",
|
|
1582
|
+
);
|
|
1583
|
+
if (source.ok !== true) return fail("Ghostget omni response.ok", "must be true");
|
|
1584
|
+
if (source.schemaVersion !== 1) {
|
|
1585
|
+
return fail("Ghostget omni response.schemaVersion", "must be 1");
|
|
1586
|
+
}
|
|
1587
|
+
const parsedIdentity = identity(source.identity);
|
|
1588
|
+
if (parsedIdentity.invocationDigest !== request.invocationDigest) {
|
|
1589
|
+
return fail(
|
|
1590
|
+
"Ghostget omni response identity.invocationDigest",
|
|
1591
|
+
"does not match the exact canonical invocation",
|
|
1592
|
+
);
|
|
1593
|
+
}
|
|
1594
|
+
if (parsedIdentity.requestDigest !== request.requestDigest) {
|
|
1595
|
+
return fail(
|
|
1596
|
+
"Ghostget omni response identity.requestDigest",
|
|
1597
|
+
"does not match the cursor-independent canonical request",
|
|
1598
|
+
);
|
|
1599
|
+
}
|
|
1600
|
+
const parsedSource = oneOf(
|
|
1601
|
+
source.source,
|
|
1602
|
+
["omni-cache", "omni-live", "omni-exact-cache", "omni-identity"] as const,
|
|
1603
|
+
"Ghostget omni response.source",
|
|
1604
|
+
);
|
|
1605
|
+
if (parsedSource === "omni-identity") {
|
|
1606
|
+
if (source.view !== null) return fail("Ghostget omni response.view", "must be null for identity-only output");
|
|
1607
|
+
return Object.freeze({
|
|
1608
|
+
source: parsedSource,
|
|
1609
|
+
identity: parsedIdentity,
|
|
1610
|
+
view: null,
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
if (source.view === null) return fail("Ghostget omni response.view", "is required");
|
|
1614
|
+
return Object.freeze({
|
|
1615
|
+
source: parsedSource,
|
|
1616
|
+
identity: parsedIdentity,
|
|
1617
|
+
view: view(source.view, request),
|
|
1618
|
+
});
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
function parseEnvelopeOutput(
|
|
1622
|
+
value: unknown,
|
|
1623
|
+
label: string,
|
|
1624
|
+
request: PreparedRequest,
|
|
1625
|
+
): ParsedEnvelope {
|
|
1626
|
+
return envelope(parseJsonOutput(childText(value, label), label), request);
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
function runSynchronousCommand(
|
|
1630
|
+
command: PreparedCommand,
|
|
1631
|
+
options: PreparedOptions,
|
|
1632
|
+
label: string,
|
|
1633
|
+
): ParsedEnvelope {
|
|
1634
|
+
requireBunRuntime();
|
|
1635
|
+
const result = spawnSync(process.execPath, command.arguments, {
|
|
1636
|
+
cwd: options.cwd,
|
|
1637
|
+
env: options.environment,
|
|
1638
|
+
input: command.input,
|
|
1639
|
+
encoding: "utf8",
|
|
1640
|
+
maxBuffer: MAX_OUTPUT_BYTES,
|
|
1641
|
+
timeout: SYNCHRONOUS_COMMAND_TIMEOUT_MS,
|
|
1642
|
+
killSignal: "SIGKILL",
|
|
1643
|
+
});
|
|
1644
|
+
if (result.error !== undefined) throw result.error;
|
|
1645
|
+
const code = result.status ?? 3;
|
|
1646
|
+
const stderr = childText(result.stderr ?? "", `${label} stderr`);
|
|
1647
|
+
if (code !== 0) {
|
|
1648
|
+
throw new Error(boundedMessage(stderr) || `${label} exited ${code}`);
|
|
1649
|
+
}
|
|
1650
|
+
const stdout = childText(result.stdout ?? "", `${label} stdout`);
|
|
1651
|
+
if (stdout.trim().length === 0) {
|
|
1652
|
+
throw new Error(boundedMessage(stderr) || `${label} returned no output`);
|
|
1653
|
+
}
|
|
1654
|
+
return parseEnvelopeOutput(stdout, `${label} response`, command.request);
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
function runAsynchronousCommand(
|
|
1658
|
+
command: PreparedCommand,
|
|
1659
|
+
options: PreparedOptions,
|
|
1660
|
+
label: string,
|
|
1661
|
+
timeoutMs?: number,
|
|
1662
|
+
): Promise<ParsedEnvelope> {
|
|
1663
|
+
requireBunRuntime();
|
|
1664
|
+
throwIfAborted(options.signal);
|
|
1665
|
+
return new Promise((resolve, reject) => {
|
|
1666
|
+
const child = spawn(process.execPath, command.arguments, {
|
|
1667
|
+
cwd: options.cwd,
|
|
1668
|
+
env: options.environment,
|
|
1669
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1670
|
+
});
|
|
1671
|
+
const stdout: Buffer[] = [];
|
|
1672
|
+
const stderr: Buffer[] = [];
|
|
1673
|
+
let stdoutBytes = 0;
|
|
1674
|
+
let stderrBytes = 0;
|
|
1675
|
+
let settled = false;
|
|
1676
|
+
let closed = false;
|
|
1677
|
+
let pendingError: Error | null = null;
|
|
1678
|
+
let terminationTimer: ReturnType<typeof setTimeout> | undefined;
|
|
1679
|
+
let forcedTerminationTimer: ReturnType<typeof setTimeout> | undefined;
|
|
1680
|
+
let controlTimer: ReturnType<typeof setTimeout> | undefined;
|
|
1681
|
+
const asError = (errorValue: unknown): Error => errorValue instanceof Error
|
|
1682
|
+
? errorValue
|
|
1683
|
+
: new Error(String(errorValue));
|
|
1684
|
+
const removeAbortListener = (): void => {
|
|
1685
|
+
if (options.signal !== undefined) {
|
|
1686
|
+
options.signal.removeEventListener("abort", onAbort);
|
|
1687
|
+
}
|
|
1688
|
+
};
|
|
1689
|
+
const clearTimers = (): void => {
|
|
1690
|
+
if (terminationTimer !== undefined) clearTimeout(terminationTimer);
|
|
1691
|
+
if (forcedTerminationTimer !== undefined) clearTimeout(forcedTerminationTimer);
|
|
1692
|
+
if (controlTimer !== undefined) clearTimeout(controlTimer);
|
|
1693
|
+
};
|
|
1694
|
+
const settleFailure = (errorValue: unknown): void => {
|
|
1695
|
+
if (settled) return;
|
|
1696
|
+
settled = true;
|
|
1697
|
+
clearTimers();
|
|
1698
|
+
removeAbortListener();
|
|
1699
|
+
reject(asError(errorValue));
|
|
1700
|
+
};
|
|
1701
|
+
const requestTermination = (errorValue: unknown): void => {
|
|
1702
|
+
if (settled || closed) return;
|
|
1703
|
+
pendingError ??= asError(errorValue);
|
|
1704
|
+
if (terminationTimer !== undefined) return;
|
|
1705
|
+
try {
|
|
1706
|
+
child.kill("SIGTERM");
|
|
1707
|
+
} catch {
|
|
1708
|
+
// The close/error events below remain the process authority.
|
|
1709
|
+
}
|
|
1710
|
+
if (settled || closed) return;
|
|
1711
|
+
terminationTimer = setTimeout(() => {
|
|
1712
|
+
if (settled || closed) return;
|
|
1713
|
+
try {
|
|
1714
|
+
child.kill("SIGKILL");
|
|
1715
|
+
} catch {
|
|
1716
|
+
// A concurrent close is handled by the close event.
|
|
1717
|
+
}
|
|
1718
|
+
if (settled || closed) return;
|
|
1719
|
+
forcedTerminationTimer = setTimeout(() => {
|
|
1720
|
+
if (settled || closed) return;
|
|
1721
|
+
child.stdin.destroy();
|
|
1722
|
+
child.stdout.destroy();
|
|
1723
|
+
child.stderr.destroy();
|
|
1724
|
+
settleFailure(new Error(
|
|
1725
|
+
`${label} child did not close after SIGKILL`,
|
|
1726
|
+
{ cause: pendingError },
|
|
1727
|
+
));
|
|
1728
|
+
}, CHILD_FORCEFUL_TERMINATION_MS);
|
|
1729
|
+
forcedTerminationTimer.unref();
|
|
1730
|
+
}, CHILD_GRACEFUL_TERMINATION_MS);
|
|
1731
|
+
terminationTimer.unref();
|
|
1732
|
+
};
|
|
1733
|
+
function onAbort(): void {
|
|
1734
|
+
if (options.signal !== undefined) {
|
|
1735
|
+
requestTermination(abortError(options.signal));
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
if (options.signal !== undefined) {
|
|
1739
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
1740
|
+
}
|
|
1741
|
+
if (timeoutMs !== undefined) {
|
|
1742
|
+
controlTimer = setTimeout(() => {
|
|
1743
|
+
requestTermination(new Error(`${label} exceeded its ${timeoutMs}ms deadline`));
|
|
1744
|
+
}, timeoutMs);
|
|
1745
|
+
controlTimer.unref();
|
|
1746
|
+
}
|
|
1747
|
+
child.stdin.on("error", (error) => {
|
|
1748
|
+
if (pendingError === null) requestTermination(error);
|
|
1749
|
+
});
|
|
1750
|
+
child.stdout.on("error", requestTermination);
|
|
1751
|
+
child.stderr.on("error", requestTermination);
|
|
1752
|
+
child.stdout.on("data", (chunkValue: Buffer | string) => {
|
|
1753
|
+
if (pendingError !== null) return;
|
|
1754
|
+
const chunk = Buffer.isBuffer(chunkValue)
|
|
1755
|
+
? chunkValue
|
|
1756
|
+
: Buffer.from(chunkValue);
|
|
1757
|
+
stdoutBytes += chunk.byteLength;
|
|
1758
|
+
if (stdoutBytes > MAX_OUTPUT_BYTES) {
|
|
1759
|
+
requestTermination(new Error(`${label} response exceeds its byte bound`));
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1762
|
+
stdout.push(chunk);
|
|
1763
|
+
});
|
|
1764
|
+
child.stderr.on("data", (chunkValue: Buffer | string) => {
|
|
1765
|
+
if (pendingError !== null) return;
|
|
1766
|
+
const chunk = Buffer.isBuffer(chunkValue)
|
|
1767
|
+
? chunkValue
|
|
1768
|
+
: Buffer.from(chunkValue);
|
|
1769
|
+
const remainingBytes = Math.max(0, MAX_ERROR_BYTES - stderrBytes);
|
|
1770
|
+
stderrBytes += chunk.byteLength;
|
|
1771
|
+
if (remainingBytes > 0) stderr.push(chunk.subarray(0, remainingBytes));
|
|
1772
|
+
});
|
|
1773
|
+
child.on("error", (error) => {
|
|
1774
|
+
if (child.pid === undefined) {
|
|
1775
|
+
settleFailure(error);
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
requestTermination(error);
|
|
1779
|
+
});
|
|
1780
|
+
child.on("close", (code, signal) => {
|
|
1781
|
+
if (settled) return;
|
|
1782
|
+
closed = true;
|
|
1783
|
+
clearTimers();
|
|
1784
|
+
removeAbortListener();
|
|
1785
|
+
if (pendingError !== null) {
|
|
1786
|
+
settleFailure(pendingError);
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
const error = boundedMessage(Buffer.concat(stderr).toString("utf8"));
|
|
1790
|
+
if (code !== 0) {
|
|
1791
|
+
settleFailure(new Error(error || `${label} exited ${code ?? signal ?? "unknown"}`));
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
const output = Buffer.concat(stdout).toString("utf8");
|
|
1795
|
+
if (output.trim().length === 0) {
|
|
1796
|
+
settleFailure(new Error(error || `${label} returned no output`));
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
let parsed: ParsedEnvelope;
|
|
1800
|
+
try {
|
|
1801
|
+
parsed = parseEnvelopeOutput(output, `${label} response`, command.request);
|
|
1802
|
+
} catch (parseError) {
|
|
1803
|
+
settleFailure(parseError);
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
settled = true;
|
|
1807
|
+
resolve(parsed);
|
|
1808
|
+
});
|
|
1809
|
+
child.stdin.end(command.input);
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
function runLiveCommand(
|
|
1814
|
+
command: PreparedCommand,
|
|
1815
|
+
options: PreparedOptions,
|
|
1816
|
+
): Promise<ParsedEnvelope> {
|
|
1817
|
+
return runAsynchronousCommand(command, options, "Ghostget omni live read");
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
function runAsynchronousControlCommand(
|
|
1821
|
+
command: PreparedCommand,
|
|
1822
|
+
options: PreparedOptions,
|
|
1823
|
+
label: string,
|
|
1824
|
+
): Promise<ParsedEnvelope> {
|
|
1825
|
+
return runAsynchronousCommand(
|
|
1826
|
+
command,
|
|
1827
|
+
options,
|
|
1828
|
+
label,
|
|
1829
|
+
ASYNC_CONTROL_COMMAND_TIMEOUT_MS,
|
|
1830
|
+
);
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
function requireEnvelopeSource<const T extends ParsedEnvelope["source"]>(
|
|
1834
|
+
value: ParsedEnvelope,
|
|
1835
|
+
expected: T,
|
|
1836
|
+
label: string,
|
|
1837
|
+
): Extract<ParsedEnvelope, { readonly source: T }> {
|
|
1838
|
+
if (value.source !== expected) return fail(label, `must have source ${expected}`);
|
|
1839
|
+
return value as Extract<ParsedEnvelope, { readonly source: T }>;
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
function cacheResult(value: ParsedEnvelope): OmniViewCacheResult {
|
|
1843
|
+
const parsed = requireEnvelopeSource(value, "omni-cache", "Ghostget omni cache response");
|
|
1844
|
+
return Object.freeze({
|
|
1845
|
+
schemaVersion: 1,
|
|
1846
|
+
source: parsed.source,
|
|
1847
|
+
identity: parsed.identity,
|
|
1848
|
+
view: parsed.view,
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
function liveResult(value: ParsedEnvelope): OmniViewLiveResult {
|
|
1853
|
+
const parsed = requireEnvelopeSource(value, "omni-live", "Ghostget omni live response");
|
|
1854
|
+
return Object.freeze({
|
|
1855
|
+
schemaVersion: 1,
|
|
1856
|
+
source: parsed.source,
|
|
1857
|
+
identity: parsed.identity,
|
|
1858
|
+
view: parsed.view,
|
|
1859
|
+
});
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
async function observeIdentity(
|
|
1863
|
+
request: PreparedRequest,
|
|
1864
|
+
options: PreparedOptions,
|
|
1865
|
+
): Promise<OmniViewIdentity> {
|
|
1866
|
+
throwIfAborted(options.signal);
|
|
1867
|
+
return requireEnvelopeSource(
|
|
1868
|
+
await runAsynchronousControlCommand(
|
|
1869
|
+
preparedCommand(request, "identity", options),
|
|
1870
|
+
options,
|
|
1871
|
+
"Ghostget omni identity preflight",
|
|
1872
|
+
),
|
|
1873
|
+
"omni-identity",
|
|
1874
|
+
"Ghostget omni identity response",
|
|
1875
|
+
).identity;
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
function readCachedPrepared(
|
|
1879
|
+
request: PreparedRequest,
|
|
1880
|
+
options: PreparedOptions,
|
|
1881
|
+
): OmniViewCacheResult {
|
|
1882
|
+
return cacheResult(runSynchronousCommand(
|
|
1883
|
+
preparedCommand(request, "cache", options),
|
|
1884
|
+
options,
|
|
1885
|
+
"Ghostget omni cache read",
|
|
1886
|
+
));
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
async function readCachedPreparedAsynchronously(
|
|
1890
|
+
request: PreparedRequest,
|
|
1891
|
+
options: PreparedOptions,
|
|
1892
|
+
): Promise<OmniViewCacheResult> {
|
|
1893
|
+
return cacheResult(await runAsynchronousControlCommand(
|
|
1894
|
+
preparedCommand(request, "cache", options),
|
|
1895
|
+
options,
|
|
1896
|
+
"Ghostget omni cache read",
|
|
1897
|
+
));
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
function identitiesMatch(left: OmniViewIdentity, right: OmniViewIdentity): boolean {
|
|
1901
|
+
return left.invocationDigest === right.invocationDigest
|
|
1902
|
+
&& left.requestDigest === right.requestDigest
|
|
1903
|
+
&& left.sourceSetDigest === right.sourceSetDigest;
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
function assertIdentity(
|
|
1907
|
+
expected: OmniViewIdentity,
|
|
1908
|
+
actual: OmniViewIdentity,
|
|
1909
|
+
phase: string,
|
|
1910
|
+
): void {
|
|
1911
|
+
if (!identitiesMatch(expected, actual)) {
|
|
1912
|
+
throw new Error(
|
|
1913
|
+
`Ghostget omni identity changed ${phase}; the live result was discarded`,
|
|
1914
|
+
);
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
function viewsHaveSameEntities(left: OmniViewV1, right: OmniViewV1): boolean {
|
|
1919
|
+
return left.entities.length === right.entities.length
|
|
1920
|
+
&& left.entities.every((entity, index) => {
|
|
1921
|
+
const other = right.entities[index];
|
|
1922
|
+
return other !== undefined
|
|
1923
|
+
&& entity.id === other.id
|
|
1924
|
+
&& entity.revision === other.revision;
|
|
1925
|
+
});
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
type PositiveSourceHeads = {
|
|
1929
|
+
readonly rootExact: string | null;
|
|
1930
|
+
readonly normalization: string | null;
|
|
1931
|
+
readonly continuationExact: string | null;
|
|
1932
|
+
};
|
|
1933
|
+
|
|
1934
|
+
function positiveSourceHeads(
|
|
1935
|
+
source: OmniViewSourceStatusV1,
|
|
1936
|
+
): PositiveSourceHeads {
|
|
1937
|
+
const rootExact = source.exact.state === "hit"
|
|
1938
|
+
? canonicalJson(Object.freeze({
|
|
1939
|
+
key: source.exact.key,
|
|
1940
|
+
dataRevision: source.exact.dataRevision,
|
|
1941
|
+
validatedAt: source.exact.validatedAt,
|
|
1942
|
+
}))
|
|
1943
|
+
: null;
|
|
1944
|
+
const continuationExact = source.normalization.state === "stale"
|
|
1945
|
+
&& source.normalization.exactQueryKey !== source.exact.key
|
|
1946
|
+
&& source.normalization.exactDataRevision !== null
|
|
1947
|
+
? canonicalJson(Object.freeze({
|
|
1948
|
+
key: source.normalization.exactQueryKey,
|
|
1949
|
+
dataRevision: source.normalization.exactDataRevision,
|
|
1950
|
+
}))
|
|
1951
|
+
: source.normalization.state === "retained-after-drift"
|
|
1952
|
+
&& source.normalization.exactQueryKey !== source.exact.key
|
|
1953
|
+
&& source.normalization.newerExactDataRevision !== null
|
|
1954
|
+
? canonicalJson(Object.freeze({
|
|
1955
|
+
key: source.normalization.exactQueryKey,
|
|
1956
|
+
dataRevision: source.normalization.newerExactDataRevision,
|
|
1957
|
+
}))
|
|
1958
|
+
: null;
|
|
1959
|
+
return Object.freeze({
|
|
1960
|
+
rootExact,
|
|
1961
|
+
normalization: source.normalizationDataRevision,
|
|
1962
|
+
continuationExact,
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
function hasPositiveAdvance(
|
|
1967
|
+
after: PositiveSourceHeads,
|
|
1968
|
+
before: PositiveSourceHeads | null,
|
|
1969
|
+
duringLive: PositiveSourceHeads,
|
|
1970
|
+
): boolean {
|
|
1971
|
+
return (Object.keys(after) as (keyof PositiveSourceHeads)[]).some((key) => {
|
|
1972
|
+
const candidate = after[key];
|
|
1973
|
+
return candidate !== null
|
|
1974
|
+
&& candidate !== duringLive[key]
|
|
1975
|
+
&& (before === null || candidate !== before[key]);
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
function laterSourceCoordinates(
|
|
1980
|
+
cachedBefore: OmniViewCacheResult | null,
|
|
1981
|
+
cachedAfter: OmniViewCacheResult,
|
|
1982
|
+
live: OmniViewLiveResult,
|
|
1983
|
+
): ReadonlySet<string> {
|
|
1984
|
+
const beforeEvidence = new Map((cachedBefore?.view.sources ?? []).map((source) => [
|
|
1985
|
+
sourceCoordinateKey(source),
|
|
1986
|
+
positiveSourceHeads(source),
|
|
1987
|
+
]));
|
|
1988
|
+
const liveEvidence = new Map(live.view.sources.map((source) => [
|
|
1989
|
+
sourceCoordinateKey(source),
|
|
1990
|
+
positiveSourceHeads(source),
|
|
1991
|
+
]));
|
|
1992
|
+
const result = new Set<string>();
|
|
1993
|
+
for (const source of cachedAfter.view.sources) {
|
|
1994
|
+
const coordinate = sourceCoordinateKey(source);
|
|
1995
|
+
const after = positiveSourceHeads(source);
|
|
1996
|
+
const before = beforeEvidence.get(coordinate) ?? null;
|
|
1997
|
+
const duringLive = liveEvidence.get(coordinate);
|
|
1998
|
+
if (
|
|
1999
|
+
duringLive !== undefined
|
|
2000
|
+
&& hasPositiveAdvance(after, before, duringLive)
|
|
2001
|
+
) result.add(coordinate);
|
|
2002
|
+
}
|
|
2003
|
+
return result;
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
function viewHasLaterRevision(
|
|
2007
|
+
cachedBefore: OmniViewCacheResult | null,
|
|
2008
|
+
cachedAfter: OmniViewCacheResult,
|
|
2009
|
+
live: OmniViewLiveResult,
|
|
2010
|
+
): boolean {
|
|
2011
|
+
return cachedAfter.view.viewRevision !== live.view.viewRevision
|
|
2012
|
+
&& (
|
|
2013
|
+
cachedBefore === null
|
|
2014
|
+
|| cachedAfter.view.viewRevision !== cachedBefore.view.viewRevision
|
|
2015
|
+
);
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
function sourceHasUnresolvedLiveIssue(
|
|
2019
|
+
source: OmniViewSourceStatusV1,
|
|
2020
|
+
): boolean {
|
|
2021
|
+
return source.exact.state === "error"
|
|
2022
|
+
|| source.normalization.state !== "current";
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
function selectCurrent(
|
|
2026
|
+
cachedBefore: OmniViewCacheResult | null,
|
|
2027
|
+
cachedAfter: OmniViewCacheResult | null,
|
|
2028
|
+
live: OmniViewLiveResult,
|
|
2029
|
+
): RevalidatedOmniViewCurrent {
|
|
2030
|
+
if (cachedAfter === null) return live;
|
|
2031
|
+
const laterSources = laterSourceCoordinates(
|
|
2032
|
+
cachedBefore,
|
|
2033
|
+
cachedAfter,
|
|
2034
|
+
live,
|
|
2035
|
+
);
|
|
2036
|
+
let useCachedView = false;
|
|
2037
|
+
if (
|
|
2038
|
+
viewHasLaterRevision(cachedBefore, cachedAfter, live)
|
|
2039
|
+
|| laterSources.size > 0
|
|
2040
|
+
) {
|
|
2041
|
+
useCachedView = true;
|
|
2042
|
+
} else if (viewsHaveSameEntities(cachedAfter.view, live.view)) {
|
|
2043
|
+
return live;
|
|
2044
|
+
} else if (
|
|
2045
|
+
cachedBefore !== null
|
|
2046
|
+
&& viewsHaveSameEntities(cachedAfter.view, cachedBefore.view)
|
|
2047
|
+
) {
|
|
2048
|
+
return live;
|
|
2049
|
+
} else {
|
|
2050
|
+
useCachedView = true;
|
|
2051
|
+
}
|
|
2052
|
+
if (!useCachedView) return live;
|
|
2053
|
+
|
|
2054
|
+
const liveSources = new Map(live.view.sources.map((source) => [
|
|
2055
|
+
sourceCoordinateKey(source),
|
|
2056
|
+
source,
|
|
2057
|
+
]));
|
|
2058
|
+
let merged = false;
|
|
2059
|
+
const sources = Object.freeze(cachedAfter.view.sources.map((cachedSource) => {
|
|
2060
|
+
const coordinate = sourceCoordinateKey(cachedSource);
|
|
2061
|
+
const liveSource = liveSources.get(coordinate);
|
|
2062
|
+
if (
|
|
2063
|
+
liveSource !== undefined
|
|
2064
|
+
&& sourceHasUnresolvedLiveIssue(liveSource)
|
|
2065
|
+
) {
|
|
2066
|
+
merged = true;
|
|
2067
|
+
return liveSource;
|
|
2068
|
+
}
|
|
2069
|
+
if (
|
|
2070
|
+
laterSources.has(coordinate)
|
|
2071
|
+
|| liveSource === undefined
|
|
2072
|
+
|| canonicalJson(liveSource) === canonicalJson(cachedSource)
|
|
2073
|
+
) return cachedSource;
|
|
2074
|
+
merged = true;
|
|
2075
|
+
return liveSource;
|
|
2076
|
+
}));
|
|
2077
|
+
if (!merged) return cachedAfter;
|
|
2078
|
+
const result: OmniViewMergedResult = Object.freeze({
|
|
2079
|
+
schemaVersion: 1,
|
|
2080
|
+
source: "omni-merged",
|
|
2081
|
+
identity: cachedAfter.identity,
|
|
2082
|
+
view: Object.freeze({
|
|
2083
|
+
...cachedAfter.view,
|
|
2084
|
+
sources,
|
|
2085
|
+
}),
|
|
2086
|
+
});
|
|
2087
|
+
return result;
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
function scheduleRevalidation(
|
|
2091
|
+
request: PreparedRequest,
|
|
2092
|
+
options: PreparedOptions,
|
|
2093
|
+
cachedBeforeSource: CachedBeforeSource,
|
|
2094
|
+
): Promise<RevalidatedOmniView> {
|
|
2095
|
+
// This callback cannot run until the public function has returned. SWR's
|
|
2096
|
+
// synchronous phase therefore performs exactly one cache-only command.
|
|
2097
|
+
return Promise.resolve().then(async () => {
|
|
2098
|
+
throwIfAborted(options.signal);
|
|
2099
|
+
const identityBefore = await observeIdentity(request, options);
|
|
2100
|
+
let cachedBefore = cachedBeforeSource.status === "provided"
|
|
2101
|
+
? cachedBeforeSource.value
|
|
2102
|
+
: null;
|
|
2103
|
+
if (cachedBeforeSource.status === "lookup") {
|
|
2104
|
+
try {
|
|
2105
|
+
cachedBefore = await readCachedPreparedAsynchronously(request, options);
|
|
2106
|
+
} catch {
|
|
2107
|
+
cachedBefore = null;
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
if (cachedBefore !== null) {
|
|
2111
|
+
assertIdentity(
|
|
2112
|
+
identityBefore,
|
|
2113
|
+
cachedBefore.identity,
|
|
2114
|
+
"before revalidation started",
|
|
2115
|
+
);
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
throwIfAborted(options.signal);
|
|
2119
|
+
const live = liveResult(await runLiveCommand(
|
|
2120
|
+
preparedCommand(request, "live", options),
|
|
2121
|
+
options,
|
|
2122
|
+
));
|
|
2123
|
+
throwIfAborted(options.signal);
|
|
2124
|
+
const identityAfter = await observeIdentity(request, options);
|
|
2125
|
+
assertIdentity(identityBefore, live.identity, "while revalidation was running");
|
|
2126
|
+
assertIdentity(identityBefore, identityAfter, "while revalidation was running");
|
|
2127
|
+
|
|
2128
|
+
let cachedAfter: OmniViewCacheResult | null = null;
|
|
2129
|
+
try {
|
|
2130
|
+
cachedAfter = await readCachedPreparedAsynchronously(request, options);
|
|
2131
|
+
} catch {
|
|
2132
|
+
cachedAfter = null;
|
|
2133
|
+
}
|
|
2134
|
+
if (cachedAfter !== null) {
|
|
2135
|
+
assertIdentity(
|
|
2136
|
+
identityBefore,
|
|
2137
|
+
cachedAfter.identity,
|
|
2138
|
+
"before the final cache observation",
|
|
2139
|
+
);
|
|
2140
|
+
}
|
|
2141
|
+
throwIfAborted(options.signal);
|
|
2142
|
+
return Object.freeze({
|
|
2143
|
+
cachedBefore,
|
|
2144
|
+
cachedAfter,
|
|
2145
|
+
live,
|
|
2146
|
+
current: selectCurrent(cachedBefore, cachedAfter, live),
|
|
2147
|
+
});
|
|
2148
|
+
});
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
export function readCachedOmniView(
|
|
2152
|
+
request: OmniViewRequest,
|
|
2153
|
+
options: ReadOmniViewOptions = {},
|
|
2154
|
+
): OmniViewCacheResult {
|
|
2155
|
+
const preparedOptions = snapshotOptions(options, false);
|
|
2156
|
+
const preparedRequest = prepareRequest(request);
|
|
2157
|
+
return readCachedPrepared(preparedRequest, preparedOptions);
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
export function revalidateOmniView(
|
|
2161
|
+
request: OmniViewRequest,
|
|
2162
|
+
options: RevalidateOmniViewOptions = {},
|
|
2163
|
+
): Promise<RevalidatedOmniView> {
|
|
2164
|
+
const preparedOptions = snapshotOptions(options, true);
|
|
2165
|
+
const preparedRequest = prepareRequest(request);
|
|
2166
|
+
return scheduleRevalidation(
|
|
2167
|
+
preparedRequest,
|
|
2168
|
+
preparedOptions,
|
|
2169
|
+
{ status: "lookup" },
|
|
2170
|
+
);
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
export function staleWhileRevalidateOmniView(
|
|
2174
|
+
request: OmniViewRequest,
|
|
2175
|
+
options: RevalidateOmniViewOptions = {},
|
|
2176
|
+
): {
|
|
2177
|
+
readonly cached: OmniViewCacheResult | null;
|
|
2178
|
+
readonly revalidation: Promise<RevalidatedOmniView>;
|
|
2179
|
+
} {
|
|
2180
|
+
const preparedOptions = snapshotOptions(options, true);
|
|
2181
|
+
const preparedRequest = prepareRequest(request);
|
|
2182
|
+
let cached: OmniViewCacheResult | null = null;
|
|
2183
|
+
try {
|
|
2184
|
+
cached = readCachedPrepared(preparedRequest, preparedOptions);
|
|
2185
|
+
} catch {
|
|
2186
|
+
cached = null;
|
|
2187
|
+
}
|
|
2188
|
+
return Object.freeze({
|
|
2189
|
+
cached,
|
|
2190
|
+
revalidation: scheduleRevalidation(
|
|
2191
|
+
preparedRequest,
|
|
2192
|
+
preparedOptions,
|
|
2193
|
+
{ status: "provided", value: cached },
|
|
2194
|
+
),
|
|
2195
|
+
});
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
export type {
|
|
2199
|
+
OmniAttachmentV1,
|
|
2200
|
+
OmniClientEnvironment,
|
|
2201
|
+
OmniConversationV1,
|
|
2202
|
+
OmniCoverageSourceStatusV1,
|
|
2203
|
+
OmniEntityKindV1,
|
|
2204
|
+
OmniEntityV1,
|
|
2205
|
+
OmniExactFreshnessV1,
|
|
2206
|
+
OmniExactSourceStatusV1,
|
|
2207
|
+
OmniJsonValue,
|
|
2208
|
+
OmniMessageV1,
|
|
2209
|
+
OmniNormalizationSourceStatusV1,
|
|
2210
|
+
OmniNotificationV1,
|
|
2211
|
+
OmniParticipantV1,
|
|
2212
|
+
OmniViewCacheResult,
|
|
2213
|
+
OmniViewExactCacheResult,
|
|
2214
|
+
OmniViewIdentity,
|
|
2215
|
+
OmniViewLiveResult,
|
|
2216
|
+
OmniViewMergedResult,
|
|
2217
|
+
OmniViewRequest,
|
|
2218
|
+
OmniViewSourceRequest,
|
|
2219
|
+
OmniViewSourceStatusV1,
|
|
2220
|
+
OmniViewV1,
|
|
2221
|
+
ReadOmniViewOptions,
|
|
2222
|
+
RevalidatedOmniView,
|
|
2223
|
+
RevalidatedOmniViewCurrent,
|
|
2224
|
+
RevalidateOmniViewOptions,
|
|
2225
|
+
} from "./omni-client-types";
|