@cardstack/boxel-cli 0.2.0 → 0.3.0-unstable.58
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/bundled-types/base/-private.d.ts +1 -0
- package/bundled-types/base/address.d.ts +17 -0
- package/bundled-types/base/ai-app-generator.d.ts +14 -0
- package/bundled-types/base/amount-with-currency.d.ts +10 -0
- package/bundled-types/base/avif-image-def.d.ts +12 -0
- package/bundled-types/base/avif-meta-extractor.d.ts +4 -0
- package/bundled-types/base/base64-image.d.ts +25 -0
- package/bundled-types/base/big-integer.d.ts +12 -0
- package/bundled-types/base/boolean.d.ts +14 -0
- package/bundled-types/base/brand-functional-palette.d.ts +16 -0
- package/bundled-types/base/brand-guide.d.ts +26 -0
- package/bundled-types/base/brand-logo.d.ts +29 -0
- package/bundled-types/base/card-api.d.ts +513 -0
- package/bundled-types/base/card-serialization.d.ts +47 -0
- package/bundled-types/base/cards-grid.d.ts +10 -0
- package/bundled-types/base/code-ref.d.ts +14 -0
- package/bundled-types/base/codemirror-editor.d.ts +135 -0
- package/bundled-types/base/color-field/components/advanced-color-picker.d.ts +66 -0
- package/bundled-types/base/color-field/components/color-picker-field.d.ts +22 -0
- package/bundled-types/base/color-field/components/color-wheel-picker.d.ts +44 -0
- package/bundled-types/base/color-field/components/contrast-checker-addon.d.ts +14 -0
- package/bundled-types/base/color-field/components/recent-colors-addon.d.ts +19 -0
- package/bundled-types/base/color-field/components/slider-picker.d.ts +40 -0
- package/bundled-types/base/color-field/components/swatches-picker.d.ts +5 -0
- package/bundled-types/base/color-field/modifiers/setup-element-modifier.d.ts +10 -0
- package/bundled-types/base/color-field/util/color-field-signature.d.ts +9 -0
- package/bundled-types/base/color-field/util/color-utils.d.ts +96 -0
- package/bundled-types/base/color-field/util/css-color-parsers.d.ts +11 -0
- package/bundled-types/base/color.d.ts +10 -0
- package/bundled-types/base/command.d.ts +593 -0
- package/bundled-types/base/commands/search-card-result.d.ts +36 -0
- package/bundled-types/base/components/age.d.ts +22 -0
- package/bundled-types/base/components/business-days.d.ts +16 -0
- package/bundled-types/base/components/card-list.d.ts +25 -0
- package/bundled-types/base/components/cards-grid-layout.d.ts +58 -0
- package/bundled-types/base/components/countdown.d.ts +31 -0
- package/bundled-types/base/components/expiration-warning.d.ts +24 -0
- package/bundled-types/base/components/time-ago.d.ts +24 -0
- package/bundled-types/base/components/time-slots.d.ts +20 -0
- package/bundled-types/base/components/timeline.d.ts +22 -0
- package/bundled-types/base/contains-many-component.d.ts +7 -0
- package/bundled-types/base/coordinate.d.ts +7 -0
- package/bundled-types/base/country.d.ts +23 -0
- package/bundled-types/base/css-value.d.ts +6 -0
- package/bundled-types/base/csv-file-def.d.ts +29 -0
- package/bundled-types/base/currency.d.ts +16 -0
- package/bundled-types/base/date/day.d.ts +9 -0
- package/bundled-types/base/date/month-day.d.ts +10 -0
- package/bundled-types/base/date/month-year.d.ts +9 -0
- package/bundled-types/base/date/month.d.ts +9 -0
- package/bundled-types/base/date/quarter.d.ts +10 -0
- package/bundled-types/base/date/week.d.ts +9 -0
- package/bundled-types/base/date/year.d.ts +9 -0
- package/bundled-types/base/date-range-field.d.ts +12 -0
- package/bundled-types/base/date.d.ts +12 -0
- package/bundled-types/base/datetime-stamp.d.ts +7 -0
- package/bundled-types/base/datetime.d.ts +12 -0
- package/bundled-types/base/default-templates/atom.d.ts +10 -0
- package/bundled-types/base/default-templates/card-info.d.ts +32 -0
- package/bundled-types/base/default-templates/embedded.d.ts +11 -0
- package/bundled-types/base/default-templates/field-edit.d.ts +10 -0
- package/bundled-types/base/default-templates/file-def-edit.d.ts +8 -0
- package/bundled-types/base/default-templates/fitted.d.ts +12 -0
- package/bundled-types/base/default-templates/head.d.ts +13 -0
- package/bundled-types/base/default-templates/image-def-atom.d.ts +8 -0
- package/bundled-types/base/default-templates/image-def-embedded.d.ts +8 -0
- package/bundled-types/base/default-templates/image-def-fitted.d.ts +8 -0
- package/bundled-types/base/default-templates/image-def-isolated.d.ts +8 -0
- package/bundled-types/base/default-templates/isolated-and-edit.d.ts +15 -0
- package/bundled-types/base/default-templates/markdown-fallback.d.ts +19 -0
- package/bundled-types/base/default-templates/markdown.d.ts +60 -0
- package/bundled-types/base/default-templates/missing-template.d.ts +13 -0
- package/bundled-types/base/default-templates/theme-dashboard.d.ts +126 -0
- package/bundled-types/base/detailed-style-reference.d.ts +48 -0
- package/bundled-types/base/email.d.ts +10 -0
- package/bundled-types/base/enum.d.ts +30 -0
- package/bundled-types/base/ethereum-address.d.ts +13 -0
- package/bundled-types/base/field-component.d.ts +65 -0
- package/bundled-types/base/field-support.d.ts +55 -0
- package/bundled-types/base/file-api.d.ts +2 -0
- package/bundled-types/base/file-menu-items.d.ts +4 -0
- package/bundled-types/base/gif-image-def.d.ts +12 -0
- package/bundled-types/base/gif-meta-extractor.d.ts +4 -0
- package/bundled-types/base/gts-file-def.d.ts +7 -0
- package/bundled-types/base/helpers/country.d.ts +3198 -0
- package/bundled-types/base/helpers/sanitized-html.d.ts +2 -0
- package/bundled-types/base/helpers/set-background-image.d.ts +2 -0
- package/bundled-types/base/image-file-def.d.ts +1 -0
- package/bundled-types/base/image.d.ts +8 -0
- package/bundled-types/base/index.d.ts +1 -0
- package/bundled-types/base/join-the-community.d.ts +14 -0
- package/bundled-types/base/jpg-image-def.d.ts +12 -0
- package/bundled-types/base/jpg-meta-extractor.d.ts +4 -0
- package/bundled-types/base/json-file-def.d.ts +23 -0
- package/bundled-types/base/links-to-editor.d.ts +22 -0
- package/bundled-types/base/links-to-many-component.d.ts +7 -0
- package/bundled-types/base/llm-model.d.ts +6 -0
- package/bundled-types/base/markdown-file-def.d.ts +25 -0
- package/bundled-types/base/markdown-helpers.d.ts +26 -0
- package/bundled-types/base/markdown.d.ts +1 -0
- package/bundled-types/base/matrix-event.d.ts +400 -0
- package/bundled-types/base/menu-items.d.ts +21 -0
- package/bundled-types/base/number/components/badge-counter.d.ts +32 -0
- package/bundled-types/base/number/components/badge-metric.d.ts +34 -0
- package/bundled-types/base/number/components/badge-notification.d.ts +38 -0
- package/bundled-types/base/number/components/gauge.d.ts +45 -0
- package/bundled-types/base/number/components/number-input.d.ts +27 -0
- package/bundled-types/base/number/components/progress-bar.d.ts +44 -0
- package/bundled-types/base/number/components/progress-circle.d.ts +41 -0
- package/bundled-types/base/number/components/score.d.ts +41 -0
- package/bundled-types/base/number/components/stat.d.ts +38 -0
- package/bundled-types/base/number/util/index.d.ts +29 -0
- package/bundled-types/base/number.d.ts +52 -0
- package/bundled-types/base/percentage.d.ts +9 -0
- package/bundled-types/base/phone-number.d.ts +25 -0
- package/bundled-types/base/png-image-def.d.ts +12 -0
- package/bundled-types/base/png-meta-extractor.d.ts +4 -0
- package/bundled-types/base/positioned-card.d.ts +7 -0
- package/bundled-types/base/query-field-support.d.ts +6 -0
- package/bundled-types/base/realm-config.d.ts +16 -0
- package/bundled-types/base/realm.d.ts +7 -0
- package/bundled-types/base/resources/command-data.d.ts +43 -0
- package/bundled-types/base/response-field.d.ts +6 -0
- package/bundled-types/base/rich-markdown.d.ts +30 -0
- package/bundled-types/base/shared-state.d.ts +1 -0
- package/bundled-types/base/skill-plus.d.ts +79 -0
- package/bundled-types/base/skill-reference.d.ts +12 -0
- package/bundled-types/base/skill-set.d.ts +18 -0
- package/bundled-types/base/skill.d.ts +21 -0
- package/bundled-types/base/spec.d.ts +108 -0
- package/bundled-types/base/string.d.ts +2 -0
- package/bundled-types/base/structured-theme-variables.d.ts +85 -0
- package/bundled-types/base/structured-theme.d.ts +32 -0
- package/bundled-types/base/style-reference.d.ts +14 -0
- package/bundled-types/base/svg-image-def.d.ts +12 -0
- package/bundled-types/base/svg-meta-extractor.d.ts +12 -0
- package/bundled-types/base/system-card.d.ts +17 -0
- package/bundled-types/base/tag.d.ts +15 -0
- package/bundled-types/base/text-area.d.ts +2 -0
- package/bundled-types/base/text-file-def.d.ts +23 -0
- package/bundled-types/base/text-input-validator.d.ts +13 -0
- package/bundled-types/base/theme.d.ts +2 -0
- package/bundled-types/base/time/duration.d.ts +14 -0
- package/bundled-types/base/time/relative-time.d.ts +10 -0
- package/bundled-types/base/time/time-range.d.ts +11 -0
- package/bundled-types/base/time.d.ts +9 -0
- package/bundled-types/base/ts-file-def.d.ts +26 -0
- package/bundled-types/base/ts-highlight.d.ts +1 -0
- package/bundled-types/base/types/@cardstack/boxel-host/index.d.ts +5 -0
- package/bundled-types/base/types/ember-css-url/index.d.ts +15 -0
- package/bundled-types/base/typography.d.ts +14 -0
- package/bundled-types/base/url.d.ts +13 -0
- package/bundled-types/base/watched-array.d.ts +7 -0
- package/bundled-types/base/webp-image-def.d.ts +12 -0
- package/bundled-types/base/webp-meta-extractor.d.ts +4 -0
- package/bundled-types/base/website.d.ts +8 -0
- package/bundled-types/base/welcome-to-boxel.d.ts +22 -0
- package/bundled-types/boxel-ui/components/accordion/index.gts +50 -0
- package/bundled-types/boxel-ui/components/accordion/item/index.gts +156 -0
- package/bundled-types/boxel-ui/components/accordion/usage.gts +157 -0
- package/bundled-types/boxel-ui/components/add-button/index.gts +46 -0
- package/bundled-types/boxel-ui/components/add-button/usage.gts +54 -0
- package/bundled-types/boxel-ui/components/alert/index.gts +151 -0
- package/bundled-types/boxel-ui/components/alert/usage.gts +66 -0
- package/bundled-types/boxel-ui/components/avatar/index.gts +79 -0
- package/bundled-types/boxel-ui/components/avatar/usage.gts +96 -0
- package/bundled-types/boxel-ui/components/basic-fitted/index.gts +340 -0
- package/bundled-types/boxel-ui/components/basic-fitted/usage.gts +223 -0
- package/bundled-types/boxel-ui/components/button/index.gts +416 -0
- package/bundled-types/boxel-ui/components/button/usage.gts +334 -0
- package/bundled-types/boxel-ui/components/card-container/index.gts +251 -0
- package/bundled-types/boxel-ui/components/card-container/usage.gts +53 -0
- package/bundled-types/boxel-ui/components/card-header/index.gts +473 -0
- package/bundled-types/boxel-ui/components/card-header/usage.gts +295 -0
- package/bundled-types/boxel-ui/components/circle-spinner/index.gts +42 -0
- package/bundled-types/boxel-ui/components/circle-spinner/usage.gts +48 -0
- package/bundled-types/boxel-ui/components/color-palette/index.gts +134 -0
- package/bundled-types/boxel-ui/components/color-palette/usage.gts +69 -0
- package/bundled-types/boxel-ui/components/color-picker/index.gts +125 -0
- package/bundled-types/boxel-ui/components/color-picker/usage.gts +56 -0
- package/bundled-types/boxel-ui/components/container/index.gts +67 -0
- package/bundled-types/boxel-ui/components/container/usage.gts +59 -0
- package/bundled-types/boxel-ui/components/context-button/index.gts +184 -0
- package/bundled-types/boxel-ui/components/context-button/usage.gts +125 -0
- package/bundled-types/boxel-ui/components/copy-button/index.gts +79 -0
- package/bundled-types/boxel-ui/components/copy-button/usage.gts +25 -0
- package/bundled-types/boxel-ui/components/date-range-picker/index.gts +211 -0
- package/bundled-types/boxel-ui/components/date-range-picker/setup.gts +11 -0
- package/bundled-types/boxel-ui/components/date-range-picker/usage.gts +52 -0
- package/bundled-types/boxel-ui/components/drag-and-drop/index.gts +256 -0
- package/bundled-types/boxel-ui/components/drag-and-drop/usage.gts +142 -0
- package/bundled-types/boxel-ui/components/dropdown/index.gts +478 -0
- package/bundled-types/boxel-ui/components/dropdown/trigger/index.gts +70 -0
- package/bundled-types/boxel-ui/components/dropdown/trigger/usage.gts +54 -0
- package/bundled-types/boxel-ui/components/dropdown/usage.gts +119 -0
- package/bundled-types/boxel-ui/components/email-input/index.gts +111 -0
- package/bundled-types/boxel-ui/components/email-input/usage.gts +75 -0
- package/bundled-types/boxel-ui/components/entity-icon-display/index.gts +144 -0
- package/bundled-types/boxel-ui/components/entity-icon-display/usage.gts +54 -0
- package/bundled-types/boxel-ui/components/entity-thumbnail-display/index.gts +148 -0
- package/bundled-types/boxel-ui/components/entity-thumbnail-display/usage.gts +76 -0
- package/bundled-types/boxel-ui/components/field-container/index.gts +150 -0
- package/bundled-types/boxel-ui/components/field-container/usage.gts +188 -0
- package/bundled-types/boxel-ui/components/filter-list/index.gts +255 -0
- package/bundled-types/boxel-ui/components/filter-list/usage.gts +153 -0
- package/bundled-types/boxel-ui/components/fitted-card-container/index.gts +66 -0
- package/bundled-types/boxel-ui/components/fitted-card-container/usage.gts +81 -0
- package/bundled-types/boxel-ui/components/grid-container/grid-item-container/index.gts +37 -0
- package/bundled-types/boxel-ui/components/grid-container/index.gts +116 -0
- package/bundled-types/boxel-ui/components/grid-container/usage.gts +105 -0
- package/bundled-types/boxel-ui/components/header/index.gts +101 -0
- package/bundled-types/boxel-ui/components/header/usage.gts +116 -0
- package/bundled-types/boxel-ui/components/icon-button/index.gts +148 -0
- package/bundled-types/boxel-ui/components/icon-button/usage.gts +249 -0
- package/bundled-types/boxel-ui/components/input/index.gts +560 -0
- package/bundled-types/boxel-ui/components/input/usage.gts +449 -0
- package/bundled-types/boxel-ui/components/input-group/accessories/index.gts +191 -0
- package/bundled-types/boxel-ui/components/input-group/controls/index.gts +54 -0
- package/bundled-types/boxel-ui/components/input-group/index.gts +347 -0
- package/bundled-types/boxel-ui/components/input-group/usage.gts +437 -0
- package/bundled-types/boxel-ui/components/kanban/card.gts +89 -0
- package/bundled-types/boxel-ui/components/kanban/column-header.gts +151 -0
- package/bundled-types/boxel-ui/components/kanban/drag.gts +1007 -0
- package/bundled-types/boxel-ui/components/kanban/engine.ts +247 -0
- package/bundled-types/boxel-ui/components/kanban/ghost.gts +64 -0
- package/bundled-types/boxel-ui/components/kanban/index.gts +16 -0
- package/bundled-types/boxel-ui/components/kanban/modifiers.gts +42 -0
- package/bundled-types/boxel-ui/components/kanban/plane-inner.gts +525 -0
- package/bundled-types/boxel-ui/components/kanban/plane.gts +163 -0
- package/bundled-types/boxel-ui/components/kanban/usage.gts +392 -0
- package/bundled-types/boxel-ui/components/label/index.gts +64 -0
- package/bundled-types/boxel-ui/components/loading-indicator/index.gts +78 -0
- package/bundled-types/boxel-ui/components/loading-indicator/usage.gts +72 -0
- package/bundled-types/boxel-ui/components/menu/index.gts +267 -0
- package/bundled-types/boxel-ui/components/menu/usage.gts +100 -0
- package/bundled-types/boxel-ui/components/message/index.gts +146 -0
- package/bundled-types/boxel-ui/components/message/usage.gts +263 -0
- package/bundled-types/boxel-ui/components/modal/index.gts +159 -0
- package/bundled-types/boxel-ui/components/modal/usage.gts +206 -0
- package/bundled-types/boxel-ui/components/multi-select/after-options.gts +59 -0
- package/bundled-types/boxel-ui/components/multi-select/index.gts +230 -0
- package/bundled-types/boxel-ui/components/multi-select/selected-item.gts +91 -0
- package/bundled-types/boxel-ui/components/multi-select/trigger.gts +190 -0
- package/bundled-types/boxel-ui/components/multi-select/usage.gts +420 -0
- package/bundled-types/boxel-ui/components/phone-input/index.gts +149 -0
- package/bundled-types/boxel-ui/components/phone-input/usage.gts +73 -0
- package/bundled-types/boxel-ui/components/picker/before-options-with-search.gts +340 -0
- package/bundled-types/boxel-ui/components/picker/index.gts +427 -0
- package/bundled-types/boxel-ui/components/picker/option-row.gts +270 -0
- package/bundled-types/boxel-ui/components/picker/selected-item.gts +171 -0
- package/bundled-types/boxel-ui/components/picker/trigger-labeled.gts +216 -0
- package/bundled-types/boxel-ui/components/picker/usage.gts +179 -0
- package/bundled-types/boxel-ui/components/pill/index.gts +231 -0
- package/bundled-types/boxel-ui/components/pill/usage.gts +230 -0
- package/bundled-types/boxel-ui/components/progress-bar/index.gts +124 -0
- package/bundled-types/boxel-ui/components/progress-bar/usage.gts +131 -0
- package/bundled-types/boxel-ui/components/progress-radial/index.gts +95 -0
- package/bundled-types/boxel-ui/components/progress-radial/usage.gts +79 -0
- package/bundled-types/boxel-ui/components/radio-input/index.gts +143 -0
- package/bundled-types/boxel-ui/components/radio-input/item.gts +210 -0
- package/bundled-types/boxel-ui/components/radio-input/usage.gts +303 -0
- package/bundled-types/boxel-ui/components/realm-icon/index.gts +129 -0
- package/bundled-types/boxel-ui/components/realm-icon/usage.gts +62 -0
- package/bundled-types/boxel-ui/components/resizable-panel-group/handle.gts +180 -0
- package/bundled-types/boxel-ui/components/resizable-panel-group/index.gts +341 -0
- package/bundled-types/boxel-ui/components/resizable-panel-group/panel.gts +86 -0
- package/bundled-types/boxel-ui/components/resizable-panel-group/usage.gts +350 -0
- package/bundled-types/boxel-ui/components/resizable-panel-group/utils/adjust-layout-by-delta.ts +231 -0
- package/bundled-types/boxel-ui/components/resizable-panel-group/utils/assert.ts +10 -0
- package/bundled-types/boxel-ui/components/resizable-panel-group/utils/calculate-delta-percentage.ts +41 -0
- package/bundled-types/boxel-ui/components/resizable-panel-group/utils/calculate-unsafe-default-layout.ts +53 -0
- package/bundled-types/boxel-ui/components/resizable-panel-group/utils/compare-layouts.ts +12 -0
- package/bundled-types/boxel-ui/components/resizable-panel-group/utils/const.ts +6 -0
- package/bundled-types/boxel-ui/components/resizable-panel-group/utils/determine-pivot-indices.ts +15 -0
- package/bundled-types/boxel-ui/components/resizable-panel-group/utils/dom/get-panel-element.ts +10 -0
- package/bundled-types/boxel-ui/components/resizable-panel-group/utils/dom/get-panel-elements-for-group.ts +8 -0
- package/bundled-types/boxel-ui/components/resizable-panel-group/utils/dom/get-panel-group-element.ts +21 -0
- package/bundled-types/boxel-ui/components/resizable-panel-group/utils/dom/get-resize-handle-element-index.ts +14 -0
- package/bundled-types/boxel-ui/components/resizable-panel-group/utils/dom/get-resize-handle-element.ts +12 -0
- package/bundled-types/boxel-ui/components/resizable-panel-group/utils/dom/get-resize-handle-elements-for-group.ts +10 -0
- package/bundled-types/boxel-ui/components/resizable-panel-group/utils/fuzzy-layouts-equal.ts +22 -0
- package/bundled-types/boxel-ui/components/resizable-panel-group/utils/fuzzy-numbers.ts +21 -0
- package/bundled-types/boxel-ui/components/resizable-panel-group/utils/get-resize-event-coordinates.ts +8 -0
- package/bundled-types/boxel-ui/components/resizable-panel-group/utils/get-resize-event-cursor-position.ts +10 -0
- package/bundled-types/boxel-ui/components/resizable-panel-group/utils/panel-resize-handle-registry.ts +446 -0
- package/bundled-types/boxel-ui/components/resizable-panel-group/utils/resize-panel.ts +35 -0
- package/bundled-types/boxel-ui/components/resizable-panel-group/utils/types.ts +23 -0
- package/bundled-types/boxel-ui/components/resizable-panel-group/utils/validate-panel-group-layout.ts +86 -0
- package/bundled-types/boxel-ui/components/select/index.gts +882 -0
- package/bundled-types/boxel-ui/components/select/trigger.gts +132 -0
- package/bundled-types/boxel-ui/components/select/usage.gts +353 -0
- package/bundled-types/boxel-ui/components/skeleton-placeholder/index.gts +89 -0
- package/bundled-types/boxel-ui/components/skeleton-placeholder/usage.gts +102 -0
- package/bundled-types/boxel-ui/components/sort-dropdown/index.gts +91 -0
- package/bundled-types/boxel-ui/components/sort-dropdown/usage.gts +77 -0
- package/bundled-types/boxel-ui/components/swatch/index.gts +81 -0
- package/bundled-types/boxel-ui/components/swatch/usage.gts +57 -0
- package/bundled-types/boxel-ui/components/switch/index.gts +95 -0
- package/bundled-types/boxel-ui/components/switch/usage.gts +79 -0
- package/bundled-types/boxel-ui/components/tabbed-header/index.gts +141 -0
- package/bundled-types/boxel-ui/components/tabbed-header/usage.gts +103 -0
- package/bundled-types/boxel-ui/components/tag/index.gts +50 -0
- package/bundled-types/boxel-ui/components/tag/usage.gts +89 -0
- package/bundled-types/boxel-ui/components/tag-list/index.gts +85 -0
- package/bundled-types/boxel-ui/components/tag-list/usage.gts +129 -0
- package/bundled-types/boxel-ui/components/tooltip/index.gts +306 -0
- package/bundled-types/boxel-ui/components/tooltip/usage.gts +168 -0
- package/bundled-types/boxel-ui/components/view-selector/index.gts +166 -0
- package/bundled-types/boxel-ui/components/view-selector/usage.gts +149 -0
- package/bundled-types/boxel-ui/components.ts +169 -0
- package/bundled-types/boxel-ui/helpers/add-class-to-svg.ts +8 -0
- package/bundled-types/boxel-ui/helpers/clipboard.ts +21 -0
- package/bundled-types/boxel-ui/helpers/cn.ts +12 -0
- package/bundled-types/boxel-ui/helpers/color-tools.ts +331 -0
- package/bundled-types/boxel-ui/helpers/compact.ts +3 -0
- package/bundled-types/boxel-ui/helpers/contrast-color.ts +32 -0
- package/bundled-types/boxel-ui/helpers/css-var.ts +41 -0
- package/bundled-types/boxel-ui/helpers/currency-format.ts +25 -0
- package/bundled-types/boxel-ui/helpers/dayjs-format.ts +27 -0
- package/bundled-types/boxel-ui/helpers/deterministic-color-from-string.ts +45 -0
- package/bundled-types/boxel-ui/helpers/element.ts +16 -0
- package/bundled-types/boxel-ui/helpers/extract-css-variables.ts +126 -0
- package/bundled-types/boxel-ui/helpers/format-age.ts +72 -0
- package/bundled-types/boxel-ui/helpers/format-countdown.ts +110 -0
- package/bundled-types/boxel-ui/helpers/format-currency.ts +95 -0
- package/bundled-types/boxel-ui/helpers/format-date-time.ts +567 -0
- package/bundled-types/boxel-ui/helpers/format-duration.ts +143 -0
- package/bundled-types/boxel-ui/helpers/format-file-size.ts +49 -0
- package/bundled-types/boxel-ui/helpers/format-list.ts +73 -0
- package/bundled-types/boxel-ui/helpers/format-names.ts +103 -0
- package/bundled-types/boxel-ui/helpers/format-number.ts +140 -0
- package/bundled-types/boxel-ui/helpers/format-ordinal.ts +115 -0
- package/bundled-types/boxel-ui/helpers/format-period.ts +188 -0
- package/bundled-types/boxel-ui/helpers/format-relative-time.ts +789 -0
- package/bundled-types/boxel-ui/helpers/generate-css-variables.ts +47 -0
- package/bundled-types/boxel-ui/helpers/markdown-escape.ts +36 -0
- package/bundled-types/boxel-ui/helpers/math-helpers.ts +18 -0
- package/bundled-types/boxel-ui/helpers/menu-divider.ts +15 -0
- package/bundled-types/boxel-ui/helpers/menu-item.ts +76 -0
- package/bundled-types/boxel-ui/helpers/optional.ts +7 -0
- package/bundled-types/boxel-ui/helpers/pick.ts +11 -0
- package/bundled-types/boxel-ui/helpers/sanitize-html.ts +45 -0
- package/bundled-types/boxel-ui/helpers/string.ts +15 -0
- package/bundled-types/boxel-ui/helpers/theme-css.ts +148 -0
- package/bundled-types/boxel-ui/helpers/truth-helpers.ts +48 -0
- package/bundled-types/boxel-ui/helpers/validate-email-format.ts +138 -0
- package/bundled-types/boxel-ui/helpers/validate-phone-format.ts +201 -0
- package/bundled-types/boxel-ui/helpers.ts +134 -0
- package/bundled-types/boxel-ui/icons/ai-bw.gts +22 -0
- package/bundled-types/boxel-ui/icons/arrow-left.gts +25 -0
- package/bundled-types/boxel-ui/icons/arrow-right.gts +28 -0
- package/bundled-types/boxel-ui/icons/arrow-top-left.gts +22 -0
- package/bundled-types/boxel-ui/icons/arrow-up.gts +25 -0
- package/bundled-types/boxel-ui/icons/atom.gts +26 -0
- package/bundled-types/boxel-ui/icons/boxel-icon-with-text.gts +29 -0
- package/bundled-types/boxel-ui/icons/boxel-icon.gts +34 -0
- package/bundled-types/boxel-ui/icons/card-definition.gts +26 -0
- package/bundled-types/boxel-ui/icons/card-instance.gts +26 -0
- package/bundled-types/boxel-ui/icons/card.gts +24 -0
- package/bundled-types/boxel-ui/icons/cardbot-lg.gts +24 -0
- package/bundled-types/boxel-ui/icons/caret-down.gts +23 -0
- package/bundled-types/boxel-ui/icons/caret-up.gts +23 -0
- package/bundled-types/boxel-ui/icons/check-mark.gts +23 -0
- package/bundled-types/boxel-ui/icons/chevron-right.gts +23 -0
- package/bundled-types/boxel-ui/icons/copy.gts +28 -0
- package/bundled-types/boxel-ui/icons/diagonal-arrow-left-up.gts +21 -0
- package/bundled-types/boxel-ui/icons/download.gts +19 -0
- package/bundled-types/boxel-ui/icons/dropdown-arrow-down.gts +20 -0
- package/bundled-types/boxel-ui/icons/dropdown-arrow-filled.gts +19 -0
- package/bundled-types/boxel-ui/icons/dropdown-arrow-up.gts +20 -0
- package/bundled-types/boxel-ui/icons/edit.gts +14 -0
- package/bundled-types/boxel-ui/icons/embedded.gts +34 -0
- package/bundled-types/boxel-ui/icons/exclamation-circle.gts +26 -0
- package/bundled-types/boxel-ui/icons/exclamation.gts +20 -0
- package/bundled-types/boxel-ui/icons/eye.gts +19 -0
- package/bundled-types/boxel-ui/icons/failure-bordered.gts +26 -0
- package/bundled-types/boxel-ui/icons/field.gts +26 -0
- package/bundled-types/boxel-ui/icons/file-alert.gts +28 -0
- package/bundled-types/boxel-ui/icons/file.gts +27 -0
- package/bundled-types/boxel-ui/icons/fitted.gts +42 -0
- package/bundled-types/boxel-ui/icons/folder.gts +26 -0
- package/bundled-types/boxel-ui/icons/form.gts +38 -0
- package/bundled-types/boxel-ui/icons/four-lines.gts +21 -0
- package/bundled-types/boxel-ui/icons/grid-3x3.gts +26 -0
- package/bundled-types/boxel-ui/icons/group.gts +22 -0
- package/bundled-types/boxel-ui/icons/head.gts +28 -0
- package/bundled-types/boxel-ui/icons/highlight-icon.gts +46 -0
- package/bundled-types/boxel-ui/icons/icon-circle-selected.gts +25 -0
- package/bundled-types/boxel-ui/icons/icon-circle.gts +25 -0
- package/bundled-types/boxel-ui/icons/icon-code.gts +22 -0
- package/bundled-types/boxel-ui/icons/icon-funnel.gts +23 -0
- package/bundled-types/boxel-ui/icons/icon-globe.gts +19 -0
- package/bundled-types/boxel-ui/icons/icon-grid.gts +68 -0
- package/bundled-types/boxel-ui/icons/icon-hexagon.gts +22 -0
- package/bundled-types/boxel-ui/icons/icon-inherit.gts +19 -0
- package/bundled-types/boxel-ui/icons/icon-link.gts +20 -0
- package/bundled-types/boxel-ui/icons/icon-list.gts +40 -0
- package/bundled-types/boxel-ui/icons/icon-minus-circle.gts +26 -0
- package/bundled-types/boxel-ui/icons/icon-pencil-crossed-out.gts +33 -0
- package/bundled-types/boxel-ui/icons/icon-pencil-not-crossed-out.gts +21 -0
- package/bundled-types/boxel-ui/icons/icon-pencil.gts +23 -0
- package/bundled-types/boxel-ui/icons/icon-plus-circle.gts +31 -0
- package/bundled-types/boxel-ui/icons/icon-plus-thin.gts +19 -0
- package/bundled-types/boxel-ui/icons/icon-plus.gts +20 -0
- package/bundled-types/boxel-ui/icons/icon-search-thick.gts +27 -0
- package/bundled-types/boxel-ui/icons/icon-search.gts +25 -0
- package/bundled-types/boxel-ui/icons/icon-table.gts +44 -0
- package/bundled-types/boxel-ui/icons/icon-trash.gts +24 -0
- package/bundled-types/boxel-ui/icons/icon-turn-down-right.gts +18 -0
- package/bundled-types/boxel-ui/icons/icon-x.gts +21 -0
- package/bundled-types/boxel-ui/icons/image-placeholder.gts +30 -0
- package/bundled-types/boxel-ui/icons/isolated.gts +20 -0
- package/bundled-types/boxel-ui/icons/loading-indicator.gts +26 -0
- package/bundled-types/boxel-ui/icons/lock.gts +23 -0
- package/bundled-types/boxel-ui/icons/markdown.gts +19 -0
- package/bundled-types/boxel-ui/icons/profile.gts +23 -0
- package/bundled-types/boxel-ui/icons/publish-site-icon.gts +25 -0
- package/bundled-types/boxel-ui/icons/rows-4.gts +26 -0
- package/bundled-types/boxel-ui/icons/select-all.gts +25 -0
- package/bundled-types/boxel-ui/icons/send.gts +21 -0
- package/bundled-types/boxel-ui/icons/sparkle.gts +21 -0
- package/bundled-types/boxel-ui/icons/star-filled.gts +25 -0
- package/bundled-types/boxel-ui/icons/star-half-fill.gts +27 -0
- package/bundled-types/boxel-ui/icons/star.gts +25 -0
- package/bundled-types/boxel-ui/icons/success-bordered.gts +30 -0
- package/bundled-types/boxel-ui/icons/three-dots-horizontal.gts +21 -0
- package/bundled-types/boxel-ui/icons/triangle-left.gts +23 -0
- package/bundled-types/boxel-ui/icons/triangle-right.gts +23 -0
- package/bundled-types/boxel-ui/icons/types.ts +10 -0
- package/bundled-types/boxel-ui/icons/upload.gts +20 -0
- package/bundled-types/boxel-ui/icons/warning.gts +19 -0
- package/bundled-types/boxel-ui/icons.gts +246 -0
- package/bundled-types/boxel-ui/modifiers/auto-focus.ts +7 -0
- package/bundled-types/boxel-ui/modifiers/set-css-var.ts +24 -0
- package/bundled-types/boxel-ui/modifiers.ts +14 -0
- package/bundled-types/boxel-ui/types/@cardstack/boxel-ui/index.d.ts +3 -0
- package/bundled-types/boxel-ui/types/ember-css-url/index.d.ts +15 -0
- package/bundled-types/boxel-ui/types/ember-draggable-modifiers/index.d.ts +109 -0
- package/bundled-types/boxel-ui/types/ember-draggable-modifiers/utils.d.ts +7 -0
- package/bundled-types/boxel-ui/types/ember-focus-trap/index.d.ts +20 -0
- package/bundled-types/boxel-ui/types/ember-power-calendar/components/index.d.ts +9 -0
- package/bundled-types/boxel-ui/types/ember-resize-modifier/index.d.ts +14 -0
- package/bundled-types/boxel-ui/types/ember-set-body-class/index.d.ts +12 -0
- package/bundled-types/boxel-ui/types/ember-sortable/index.d.ts +52 -0
- package/bundled-types/boxel-ui/types/global.d.ts +7 -0
- package/bundled-types/boxel-ui/usage.ts +112 -0
- package/bundled-types/boxel-ui/utils/date-utils.ts +192 -0
- package/bundled-types/boxel-ui/utils/fitted-formats.ts +161 -0
- package/bundled-types/host-app/app.ts +22 -0
- package/bundled-types/host-app/commands/add-field-to-card-definition.ts +74 -0
- package/bundled-types/host-app/commands/ai-assistant.ts +203 -0
- package/bundled-types/host-app/commands/apply-markdown-edit.ts +203 -0
- package/bundled-types/host-app/commands/apply-search-replace-block.ts +225 -0
- package/bundled-types/host-app/commands/ask-ai.ts +66 -0
- package/bundled-types/host-app/commands/authed-fetch.ts +53 -0
- package/bundled-types/host-app/commands/bot-requests/create-listing-pr-request.ts +77 -0
- package/bundled-types/host-app/commands/bot-requests/send-bot-trigger-event.ts +53 -0
- package/bundled-types/host-app/commands/can-read-realm.ts +34 -0
- package/bundled-types/host-app/commands/cancel-indexing-job.ts +29 -0
- package/bundled-types/host-app/commands/check-correctness.ts +309 -0
- package/bundled-types/host-app/commands/copy-and-edit.ts +311 -0
- package/bundled-types/host-app/commands/copy-card-as-markdown.ts +41 -0
- package/bundled-types/host-app/commands/copy-card-to-stack.ts +70 -0
- package/bundled-types/host-app/commands/copy-card.ts +67 -0
- package/bundled-types/host-app/commands/copy-file-to-realm.ts +82 -0
- package/bundled-types/host-app/commands/copy-source.ts +46 -0
- package/bundled-types/host-app/commands/create-ai-assistant-room.ts +144 -0
- package/bundled-types/host-app/commands/create-and-open-submission-workflow-card.ts +30 -0
- package/bundled-types/host-app/commands/create-specs.ts +422 -0
- package/bundled-types/host-app/commands/create-submission-workflow.ts +172 -0
- package/bundled-types/host-app/commands/evaluate-module.ts +119 -0
- package/bundled-types/host-app/commands/execute-atomic-operations.ts +44 -0
- package/bundled-types/host-app/commands/fetch-card-json.ts +33 -0
- package/bundled-types/host-app/commands/full-reindex-realm.ts +30 -0
- package/bundled-types/host-app/commands/generate-example-cards.ts +298 -0
- package/bundled-types/host-app/commands/generate-readme-spec.ts +116 -0
- package/bundled-types/host-app/commands/generate-theme-example.ts +147 -0
- package/bundled-types/host-app/commands/generate-thumbnail.ts +247 -0
- package/bundled-types/host-app/commands/get-all-realm-metas.ts +38 -0
- package/bundled-types/host-app/commands/get-available-realm-identifiers.ts +29 -0
- package/bundled-types/host-app/commands/get-card-type-schema.ts +59 -0
- package/bundled-types/host-app/commands/get-card.ts +34 -0
- package/bundled-types/host-app/commands/get-catalog-realm-identifiers.ts +29 -0
- package/bundled-types/host-app/commands/get-default-writable-realm.ts +28 -0
- package/bundled-types/host-app/commands/get-events-from-room.ts +70 -0
- package/bundled-types/host-app/commands/get-realm-of-resource-identifier.ts +37 -0
- package/bundled-types/host-app/commands/get-user-system-card.ts +38 -0
- package/bundled-types/host-app/commands/index.ts +590 -0
- package/bundled-types/host-app/commands/instantiate-card.ts +185 -0
- package/bundled-types/host-app/commands/invalidate-realm-identifiers.ts +33 -0
- package/bundled-types/host-app/commands/invite-user-to-room.ts +34 -0
- package/bundled-types/host-app/commands/lint-and-fix.ts +60 -0
- package/bundled-types/host-app/commands/listing-action-build.ts +79 -0
- package/bundled-types/host-app/commands/listing-action-init.ts +106 -0
- package/bundled-types/host-app/commands/listing-create.ts +632 -0
- package/bundled-types/host-app/commands/listing-generate-example.ts +83 -0
- package/bundled-types/host-app/commands/listing-install.ts +227 -0
- package/bundled-types/host-app/commands/listing-remix.ts +150 -0
- package/bundled-types/host-app/commands/listing-update-specs.ts +116 -0
- package/bundled-types/host-app/commands/listing-use.ts +97 -0
- package/bundled-types/host-app/commands/one-shot-llm-request.ts +209 -0
- package/bundled-types/host-app/commands/open-ai-assistant-room.ts +38 -0
- package/bundled-types/host-app/commands/open-create-listing-modal.ts +52 -0
- package/bundled-types/host-app/commands/open-in-interact-mode.ts +36 -0
- package/bundled-types/host-app/commands/open-workspace.ts +38 -0
- package/bundled-types/host-app/commands/patch-card-instance.ts +108 -0
- package/bundled-types/host-app/commands/patch-code.ts +243 -0
- package/bundled-types/host-app/commands/patch-fields.ts +304 -0
- package/bundled-types/host-app/commands/patch-theme.ts +52 -0
- package/bundled-types/host-app/commands/persist-module-inspector-view.ts +41 -0
- package/bundled-types/host-app/commands/populate-with-sample-data.ts +84 -0
- package/bundled-types/host-app/commands/preview-format.ts +52 -0
- package/bundled-types/host-app/commands/read-binary-file.ts +62 -0
- package/bundled-types/host-app/commands/read-card-for-ai-assistant.ts +50 -0
- package/bundled-types/host-app/commands/read-file-for-ai-assistant.ts +46 -0
- package/bundled-types/host-app/commands/read-source.ts +46 -0
- package/bundled-types/host-app/commands/read-text-file.ts +44 -0
- package/bundled-types/host-app/commands/register-bot.ts +42 -0
- package/bundled-types/host-app/commands/reindex-realm.ts +30 -0
- package/bundled-types/host-app/commands/retry-submission-workflow.ts +119 -0
- package/bundled-types/host-app/commands/sanitize-module-list.ts +83 -0
- package/bundled-types/host-app/commands/save-card.ts +44 -0
- package/bundled-types/host-app/commands/screenshot-card.ts +148 -0
- package/bundled-types/host-app/commands/search-and-choose.ts +180 -0
- package/bundled-types/host-app/commands/search-cards.ts +102 -0
- package/bundled-types/host-app/commands/search-google-images.ts +100 -0
- package/bundled-types/host-app/commands/send-ai-assistant-message.ts +122 -0
- package/bundled-types/host-app/commands/send-request-via-proxy.ts +70 -0
- package/bundled-types/host-app/commands/serialize-card.ts +45 -0
- package/bundled-types/host-app/commands/set-active-llm.ts +39 -0
- package/bundled-types/host-app/commands/set-user-system-card.ts +29 -0
- package/bundled-types/host-app/commands/show-card.ts +102 -0
- package/bundled-types/host-app/commands/show-file.ts +39 -0
- package/bundled-types/host-app/commands/store-add.ts +37 -0
- package/bundled-types/host-app/commands/summarize-session.ts +82 -0
- package/bundled-types/host-app/commands/switch-submode.ts +112 -0
- package/bundled-types/host-app/commands/sync-openrouter-models.ts +373 -0
- package/bundled-types/host-app/commands/transform-cards.ts +59 -0
- package/bundled-types/host-app/commands/unregister-bot.ts +29 -0
- package/bundled-types/host-app/commands/update-code-path-with-selection.ts +45 -0
- package/bundled-types/host-app/commands/update-playground-selection.ts +37 -0
- package/bundled-types/host-app/commands/update-room-skills.ts +231 -0
- package/bundled-types/host-app/commands/utils.ts +178 -0
- package/bundled-types/host-app/commands/validate-realm.ts +40 -0
- package/bundled-types/host-app/commands/write-binary-file.ts +142 -0
- package/bundled-types/host-app/commands/write-text-file.ts +104 -0
- package/bundled-types/host-app/components/.gitkeep +0 -0
- package/bundled-types/host-app/components/ai-assistant/action-bar.gts +188 -0
- package/bundled-types/host-app/components/ai-assistant/ai-assist-button-active-bg.webp +0 -0
- package/bundled-types/host-app/components/ai-assistant/ai-assist-button-active-bg@2x.webp +0 -0
- package/bundled-types/host-app/components/ai-assistant/ai-assist-button-active-bg@3x.webp +0 -0
- package/bundled-types/host-app/components/ai-assistant/ai-assist-icon-animated.webp +0 -0
- package/bundled-types/host-app/components/ai-assistant/ai-assist-icon-bw.png +0 -0
- package/bundled-types/host-app/components/ai-assistant/ai-assist-icon-bw@2x.png +0 -0
- package/bundled-types/host-app/components/ai-assistant/ai-assist-icon-bw@3x.png +0 -0
- package/bundled-types/host-app/components/ai-assistant/ai-assist-icon.webp +0 -0
- package/bundled-types/host-app/components/ai-assistant/ai-assist-icon@2x.webp +0 -0
- package/bundled-types/host-app/components/ai-assistant/ai-assist-icon@3x.webp +0 -0
- package/bundled-types/host-app/components/ai-assistant/apply-button/index.gts +226 -0
- package/bundled-types/host-app/components/ai-assistant/apply-button/usage.gts +81 -0
- package/bundled-types/host-app/components/ai-assistant/attached-file-dropdown-menu.gts +179 -0
- package/bundled-types/host-app/components/ai-assistant/attachment-picker/attach-button.gts +176 -0
- package/bundled-types/host-app/components/ai-assistant/attachment-picker/attached-items.gts +271 -0
- package/bundled-types/host-app/components/ai-assistant/attachment-picker/index.gts +144 -0
- package/bundled-types/host-app/components/ai-assistant/attachment-picker/usage.gts +127 -0
- package/bundled-types/host-app/components/ai-assistant/button.gts +60 -0
- package/bundled-types/host-app/components/ai-assistant/chat-input/index.gts +203 -0
- package/bundled-types/host-app/components/ai-assistant/chat-input/usage.gts +68 -0
- package/bundled-types/host-app/components/ai-assistant/code-block/actions.gts +58 -0
- package/bundled-types/host-app/components/ai-assistant/code-block/apply-code-patch-button.gts +66 -0
- package/bundled-types/host-app/components/ai-assistant/code-block/command-header.gts +115 -0
- package/bundled-types/host-app/components/ai-assistant/code-block/diff-editor-header.gts +209 -0
- package/bundled-types/host-app/components/ai-assistant/code-block/index.gts +228 -0
- package/bundled-types/host-app/components/ai-assistant/code-block/patch-footer.gts +32 -0
- package/bundled-types/host-app/components/ai-assistant/code-block/view-code-button.gts +73 -0
- package/bundled-types/host-app/components/ai-assistant/focus-pill/index.gts +60 -0
- package/bundled-types/host-app/components/ai-assistant/focus-pill/usage.gts +45 -0
- package/bundled-types/host-app/components/ai-assistant/llm-mode-toggle.gts +113 -0
- package/bundled-types/host-app/components/ai-assistant/llm-select.gts +158 -0
- package/bundled-types/host-app/components/ai-assistant/message/aibot-message.gts +374 -0
- package/bundled-types/host-app/components/ai-assistant/message/attachments.gts +153 -0
- package/bundled-types/host-app/components/ai-assistant/message/index.gts +678 -0
- package/bundled-types/host-app/components/ai-assistant/message/meta.gts +66 -0
- package/bundled-types/host-app/components/ai-assistant/message/text-content.gts +32 -0
- package/bundled-types/host-app/components/ai-assistant/message/usage.gts +182 -0
- package/bundled-types/host-app/components/ai-assistant/message/user-message.gts +52 -0
- package/bundled-types/host-app/components/ai-assistant/new-session-button.gts +126 -0
- package/bundled-types/host-app/components/ai-assistant/new-session-settings.gts +180 -0
- package/bundled-types/host-app/components/ai-assistant/new-session.gts +83 -0
- package/bundled-types/host-app/components/ai-assistant/panel-popover.gts +102 -0
- package/bundled-types/host-app/components/ai-assistant/panel.gts +453 -0
- package/bundled-types/host-app/components/ai-assistant/past-session-item.gts +323 -0
- package/bundled-types/host-app/components/ai-assistant/past-sessions.gts +115 -0
- package/bundled-types/host-app/components/ai-assistant/rename-session.gts +144 -0
- package/bundled-types/host-app/components/ai-assistant/restore-file-modal.gts +105 -0
- package/bundled-types/host-app/components/ai-assistant/skill-menu/index.gts +190 -0
- package/bundled-types/host-app/components/ai-assistant/skill-menu/skill-toggle.gts +168 -0
- package/bundled-types/host-app/components/ai-assistant/skill-menu/usage.gts +48 -0
- package/bundled-types/host-app/components/ai-assistant/toast.gts +293 -0
- package/bundled-types/host-app/components/card-catalog/modal.gts +579 -0
- package/bundled-types/host-app/components/card-error.gts +41 -0
- package/bundled-types/host-app/components/card-instance-picker/index.gts +54 -0
- package/bundled-types/host-app/components/card-pill.gts +189 -0
- package/bundled-types/host-app/components/card-prerender.gts +880 -0
- package/bundled-types/host-app/components/card-renderer.gts +84 -0
- package/bundled-types/host-app/components/card-search/constants.ts +76 -0
- package/bundled-types/host-app/components/card-search/item-button.gts +243 -0
- package/bundled-types/host-app/components/card-search/panel.gts +150 -0
- package/bundled-types/host-app/components/card-search/search-bar.gts +218 -0
- package/bundled-types/host-app/components/card-search/search-content.gts +604 -0
- package/bundled-types/host-app/components/card-search/search-result-header.gts +165 -0
- package/bundled-types/host-app/components/card-search/search-result-section.gts +545 -0
- package/bundled-types/host-app/components/card-search/section-header.gts +117 -0
- package/bundled-types/host-app/components/editor/directory.gts +308 -0
- package/bundled-types/host-app/components/editor/file-tree.gts +69 -0
- package/bundled-types/host-app/components/editor/import-module.gts +27 -0
- package/bundled-types/host-app/components/editor/indexed-file-tree.gts +572 -0
- package/bundled-types/host-app/components/editor/recent-files.gts +141 -0
- package/bundled-types/host-app/components/file-pill.gts +206 -0
- package/bundled-types/host-app/components/head-format-preview.gts +781 -0
- package/bundled-types/host-app/components/host-mode/breadcrumb-item.gts +150 -0
- package/bundled-types/host-app/components/host-mode/breadcrumbs.gts +131 -0
- package/bundled-types/host-app/components/host-mode/card.gts +199 -0
- package/bundled-types/host-app/components/host-mode/content.gts +195 -0
- package/bundled-types/host-app/components/host-mode/stack-item.gts +310 -0
- package/bundled-types/host-app/components/host-mode/stack.gts +91 -0
- package/bundled-types/host-app/components/matrix/auth-container.gts +66 -0
- package/bundled-types/host-app/components/matrix/auth.gts +73 -0
- package/bundled-types/host-app/components/matrix/forgot-password.gts +487 -0
- package/bundled-types/host-app/components/matrix/login.gts +241 -0
- package/bundled-types/host-app/components/matrix/register-user.gts +843 -0
- package/bundled-types/host-app/components/matrix/room-message-command.gts +330 -0
- package/bundled-types/host-app/components/matrix/room-message.gts +261 -0
- package/bundled-types/host-app/components/matrix/room.gts +1872 -0
- package/bundled-types/host-app/components/matrix/user-profile.gts +99 -0
- package/bundled-types/host-app/components/modal-container.gts +171 -0
- package/bundled-types/host-app/components/operator-mode/binary-file-info.gts +77 -0
- package/bundled-types/host-app/components/operator-mode/card-adoption-chain.gts +99 -0
- package/bundled-types/host-app/components/operator-mode/card-error-detail.gts +87 -0
- package/bundled-types/host-app/components/operator-mode/card-error.gts +177 -0
- package/bundled-types/host-app/components/operator-mode/card-schema-editor.gts +616 -0
- package/bundled-types/host-app/components/operator-mode/card-url-bar.gts +197 -0
- package/bundled-types/host-app/components/operator-mode/choose-file-modal.gts +562 -0
- package/bundled-types/host-app/components/operator-mode/choose-subscription-plan-modal.gts +517 -0
- package/bundled-types/host-app/components/operator-mode/code-editor.gts +736 -0
- package/bundled-types/host-app/components/operator-mode/code-submode/editor-indicator.gts +128 -0
- package/bundled-types/host-app/components/operator-mode/code-submode/format-chooser.gts +592 -0
- package/bundled-types/host-app/components/operator-mode/code-submode/inner-container.gts +162 -0
- package/bundled-types/host-app/components/operator-mode/code-submode/left-panel-toggle.gts +267 -0
- package/bundled-types/host-app/components/operator-mode/code-submode/module-inspector.gts +836 -0
- package/bundled-types/host-app/components/operator-mode/code-submode/playground/field-chooser-modal.gts +97 -0
- package/bundled-types/host-app/components/operator-mode/code-submode/playground/instance-chooser-dropdown.gts +346 -0
- package/bundled-types/host-app/components/operator-mode/code-submode/playground/playground-background.png +0 -0
- package/bundled-types/host-app/components/operator-mode/code-submode/playground/playground-panel.gts +1274 -0
- package/bundled-types/host-app/components/operator-mode/code-submode/playground/playground-preview.gts +155 -0
- package/bundled-types/host-app/components/operator-mode/code-submode/playground/playground.gts +98 -0
- package/bundled-types/host-app/components/operator-mode/code-submode/playground/spec-search.gts +70 -0
- package/bundled-types/host-app/components/operator-mode/code-submode/schema-editor.gts +205 -0
- package/bundled-types/host-app/components/operator-mode/code-submode/spec-preview-badge.gts +72 -0
- package/bundled-types/host-app/components/operator-mode/code-submode/spec-preview.gts +432 -0
- package/bundled-types/host-app/components/operator-mode/code-submode/toggle-button.gts +72 -0
- package/bundled-types/host-app/components/operator-mode/code-submode.gts +1030 -0
- package/bundled-types/host-app/components/operator-mode/container.gts +270 -0
- package/bundled-types/host-app/components/operator-mode/context-menu-button.gts +95 -0
- package/bundled-types/host-app/components/operator-mode/copy-button.gts +277 -0
- package/bundled-types/host-app/components/operator-mode/create-file-modal.gts +1087 -0
- package/bundled-types/host-app/components/operator-mode/create-listing-modal.gts +513 -0
- package/bundled-types/host-app/components/operator-mode/definition-container/base.gts +222 -0
- package/bundled-types/host-app/components/operator-mode/definition-container/clickable.gts +59 -0
- package/bundled-types/host-app/components/operator-mode/definition-container/divider.gts +48 -0
- package/bundled-types/host-app/components/operator-mode/definition-container/index.gts +108 -0
- package/bundled-types/host-app/components/operator-mode/delete-modal.gts +123 -0
- package/bundled-types/host-app/components/operator-mode/detail-panel-selector.gts +320 -0
- package/bundled-types/host-app/components/operator-mode/detail-panel.gts +834 -0
- package/bundled-types/host-app/components/operator-mode/edit-field-modal.gts +450 -0
- package/bundled-types/host-app/components/operator-mode/error-display.gts +590 -0
- package/bundled-types/host-app/components/operator-mode/host-submode/open-site-popover.gts +127 -0
- package/bundled-types/host-app/components/operator-mode/host-submode/publishing-realm-popover.gts +159 -0
- package/bundled-types/host-app/components/operator-mode/host-submode.gts +373 -0
- package/bundled-types/host-app/components/operator-mode/interact-submode/neighbor-stack-trigger.gts +116 -0
- package/bundled-types/host-app/components/operator-mode/interact-submode.gts +1021 -0
- package/bundled-types/host-app/components/operator-mode/new-file-button.gts +187 -0
- package/bundled-types/host-app/components/operator-mode/operator-mode-overlays.gts +511 -0
- package/bundled-types/host-app/components/operator-mode/overlays.gts +342 -0
- package/bundled-types/host-app/components/operator-mode/preview-panel/fitted-format-gallery.gts +127 -0
- package/bundled-types/host-app/components/operator-mode/preview-panel/index.gts +443 -0
- package/bundled-types/host-app/components/operator-mode/preview-panel/markdown-preview.gts +175 -0
- package/bundled-types/host-app/components/operator-mode/preview-panel/metadata-panel.gts +214 -0
- package/bundled-types/host-app/components/operator-mode/preview-panel/rendered-markdown.gts +718 -0
- package/bundled-types/host-app/components/operator-mode/private-dependency-violation.gts +70 -0
- package/bundled-types/host-app/components/operator-mode/profile/profile-email.gts +686 -0
- package/bundled-types/host-app/components/operator-mode/profile/profile-settings-modal.gts +454 -0
- package/bundled-types/host-app/components/operator-mode/profile/profile-subscription.gts +170 -0
- package/bundled-types/host-app/components/operator-mode/profile-info-popover.gts +263 -0
- package/bundled-types/host-app/components/operator-mode/publish-realm-modal.gts +1529 -0
- package/bundled-types/host-app/components/operator-mode/remove-field-modal.gts +135 -0
- package/bundled-types/host-app/components/operator-mode/send-error-to-ai-assistant.gts +195 -0
- package/bundled-types/host-app/components/operator-mode/stack-item.gts +1341 -0
- package/bundled-types/host-app/components/operator-mode/stack.gts +191 -0
- package/bundled-types/host-app/components/operator-mode/submode-layout.gts +781 -0
- package/bundled-types/host-app/components/operator-mode/syntax-error-display.gts +72 -0
- package/bundled-types/host-app/components/operator-mode/workspace-chooser/add-workspace.gts +282 -0
- package/bundled-types/host-app/components/operator-mode/workspace-chooser/index.gts +307 -0
- package/bundled-types/host-app/components/operator-mode/workspace-chooser/item-container.gts +50 -0
- package/bundled-types/host-app/components/operator-mode/workspace-chooser/workspace-loading-indicator.gts +50 -0
- package/bundled-types/host-app/components/operator-mode/workspace-chooser/workspace.gts +1092 -0
- package/bundled-types/host-app/components/pill-menu/index.gts +281 -0
- package/bundled-types/host-app/components/pill-menu/usage.gts +64 -0
- package/bundled-types/host-app/components/prerendered-card-search.gts +274 -0
- package/bundled-types/host-app/components/realm-dropdown.gts +209 -0
- package/bundled-types/host-app/components/realm-picker/index.gts +142 -0
- package/bundled-types/host-app/components/search-sheet/index.gts +426 -0
- package/bundled-types/host-app/components/search-sheet/usage.gts +99 -0
- package/bundled-types/host-app/components/submode-switcher.gts +260 -0
- package/bundled-types/host-app/components/type-picker/index.gts +111 -0
- package/bundled-types/host-app/components/with-known-realms-loaded.gts +47 -0
- package/bundled-types/host-app/components/with-loaded-realm.gts +39 -0
- package/bundled-types/host-app/components/with-subscription-data.gts +247 -0
- package/bundled-types/host-app/config/environment.ts +66 -0
- package/bundled-types/host-app/deprecation-workflow.js +30 -0
- package/bundled-types/host-app/lib/browser-queue.ts +198 -0
- package/bundled-types/host-app/lib/codemirror-context.ts +1081 -0
- package/bundled-types/host-app/lib/command-definitions.ts +41 -0
- package/bundled-types/host-app/lib/download-realm.ts +9 -0
- package/bundled-types/host-app/lib/example-card-helpers.ts +99 -0
- package/bundled-types/host-app/lib/externals.ts +247 -0
- package/bundled-types/host-app/lib/field-path-parser.ts +520 -0
- package/bundled-types/host-app/lib/file-def-manager.ts +712 -0
- package/bundled-types/host-app/lib/file-upload-state.ts +6 -0
- package/bundled-types/host-app/lib/formatted-message/utils.ts +272 -0
- package/bundled-types/host-app/lib/gc-card-store.ts +1084 -0
- package/bundled-types/host-app/lib/host-base-command.ts +29 -0
- package/bundled-types/host-app/lib/html-component.ts +144 -0
- package/bundled-types/host-app/lib/html-to-markdown.ts +96 -0
- package/bundled-types/host-app/lib/isolated-render.gts +90 -0
- package/bundled-types/host-app/lib/known-file-meta-urls.ts +17 -0
- package/bundled-types/host-app/lib/limited-set.ts +68 -0
- package/bundled-types/host-app/lib/matrix-classes/member.ts +28 -0
- package/bundled-types/host-app/lib/matrix-classes/message-builder.ts +444 -0
- package/bundled-types/host-app/lib/matrix-classes/message-code-patch-result.ts +44 -0
- package/bundled-types/host-app/lib/matrix-classes/message-command.ts +109 -0
- package/bundled-types/host-app/lib/matrix-classes/message.ts +239 -0
- package/bundled-types/host-app/lib/matrix-classes/room.ts +174 -0
- package/bundled-types/host-app/lib/matrix-utils.ts +114 -0
- package/bundled-types/host-app/lib/mutex.ts +62 -0
- package/bundled-types/host-app/lib/prerender-fetch-headers.ts +46 -0
- package/bundled-types/host-app/lib/prerender-util.ts +31 -0
- package/bundled-types/host-app/lib/public-path.ts +1 -0
- package/bundled-types/host-app/lib/random-name.ts +26 -0
- package/bundled-types/host-app/lib/realm-utils.ts +45 -0
- package/bundled-types/host-app/lib/search-cache-key.ts +33 -0
- package/bundled-types/host-app/lib/search-in-flight-key.ts +28 -0
- package/bundled-types/host-app/lib/search-replace-block-parsing.ts +101 -0
- package/bundled-types/host-app/lib/setup-globals.ts +15 -0
- package/bundled-types/host-app/lib/sqlite-adapter.ts +510 -0
- package/bundled-types/host-app/lib/stack-item.ts +164 -0
- package/bundled-types/host-app/lib/utils.ts +84 -0
- package/bundled-types/host-app/lib/window-error-handler.ts +132 -0
- package/bundled-types/host-app/router.ts +31 -0
- package/bundled-types/host-app/services/ai-assistant-panel-service.ts +850 -0
- package/bundled-types/host-app/services/billing-service.ts +208 -0
- package/bundled-types/host-app/services/card-service.ts +373 -0
- package/bundled-types/host-app/services/card-type-service.ts +226 -0
- package/bundled-types/host-app/services/code-semantics-service.ts +297 -0
- package/bundled-types/host-app/services/command-service.ts +975 -0
- package/bundled-types/host-app/services/environment-service.ts +26 -0
- package/bundled-types/host-app/services/error-display.ts +42 -0
- package/bundled-types/host-app/services/file-upload.ts +212 -0
- package/bundled-types/host-app/services/host-mode-service.ts +400 -0
- package/bundled-types/host-app/services/host-mode-state-service.ts +160 -0
- package/bundled-types/host-app/services/loader-service.ts +148 -0
- package/bundled-types/host-app/services/local-indexer.ts +68 -0
- package/bundled-types/host-app/services/local-persistence-service.ts +296 -0
- package/bundled-types/host-app/services/logger-service.ts +13 -0
- package/bundled-types/host-app/services/matrix-sdk-loader.ts +346 -0
- package/bundled-types/host-app/services/matrix-service.ts +2288 -0
- package/bundled-types/host-app/services/message-service.ts +84 -0
- package/bundled-types/host-app/services/module-contents-service.ts +333 -0
- package/bundled-types/host-app/services/monaco-service.ts +333 -0
- package/bundled-types/host-app/services/network.ts +101 -0
- package/bundled-types/host-app/services/operator-mode-state-service.ts +1593 -0
- package/bundled-types/host-app/services/playground-panel-service.ts +236 -0
- package/bundled-types/host-app/services/queue.ts +30 -0
- package/bundled-types/host-app/services/realm-info-service.ts +37 -0
- package/bundled-types/host-app/services/realm-server.ts +1211 -0
- package/bundled-types/host-app/services/realm.ts +1413 -0
- package/bundled-types/host-app/services/recent-cards-service.ts +174 -0
- package/bundled-types/host-app/services/recent-files-service.ts +224 -0
- package/bundled-types/host-app/services/render-error-state.ts +55 -0
- package/bundled-types/host-app/services/render-service.ts +362 -0
- package/bundled-types/host-app/services/render-store.ts +27 -0
- package/bundled-types/host-app/services/reset.ts +19 -0
- package/bundled-types/host-app/services/scroll-position-service.ts +81 -0
- package/bundled-types/host-app/services/spec-panel-service.ts +107 -0
- package/bundled-types/host-app/services/store.ts +2540 -0
- package/bundled-types/host-app/utils/assert-never.ts +3 -0
- package/bundled-types/host-app/utils/auth-error-guard.ts +183 -0
- package/bundled-types/host-app/utils/auth-service-worker-registration.ts +123 -0
- package/bundled-types/host-app/utils/card-search/query-builder.ts +169 -0
- package/bundled-types/host-app/utils/card-search/section-pagination.ts +57 -0
- package/bundled-types/host-app/utils/card-search/sections.ts +240 -0
- package/bundled-types/host-app/utils/card-search/type-filter.ts +124 -0
- package/bundled-types/host-app/utils/card-search/types.ts +11 -0
- package/bundled-types/host-app/utils/card-search/url.ts +27 -0
- package/bundled-types/host-app/utils/clipboard.ts +23 -0
- package/bundled-types/host-app/utils/editor/boxel-formatter.ts +65 -0
- package/bundled-types/host-app/utils/editor/editor-language.ts +88 -0
- package/bundled-types/host-app/utils/editor/monaco-test-waiter.ts +302 -0
- package/bundled-types/host-app/utils/file-def-attributes-extractor.ts +438 -0
- package/bundled-types/host-app/utils/file-name.ts +38 -0
- package/bundled-types/host-app/utils/id-from-card-or-url.ts +11 -0
- package/bundled-types/host-app/utils/lint-formatting.ts +57 -0
- package/bundled-types/host-app/utils/local-storage-keys.ts +28 -0
- package/bundled-types/host-app/utils/normalized-dir-path.ts +3 -0
- package/bundled-types/host-app/utils/prettify-messages.ts +21 -0
- package/bundled-types/host-app/utils/prettify-prompts.ts +21 -0
- package/bundled-types/host-app/utils/realm-index-boilerplate.ts +27 -0
- package/bundled-types/host-app/utils/register-boxel-transition.ts +17 -0
- package/bundled-types/host-app/utils/render-desync-detector.ts +362 -0
- package/bundled-types/host-app/utils/render-error.ts +455 -0
- package/bundled-types/host-app/utils/render-timer-stub.ts +249 -0
- package/bundled-types/host-app/utils/run-while-active.ts +14 -0
- package/bundled-types/host-app/utils/schema-editor.ts +25 -0
- package/bundled-types/host-app/utils/text-suggestion.ts +82 -0
- package/bundled-types/host-app/utils/titleize.ts +10 -0
- package/bundled-types/host-app/utils/uuid.ts +13 -0
- package/bundled-types/host-tests/helpers/adapter.ts +464 -0
- package/bundled-types/host-tests/helpers/base-realm.ts +464 -0
- package/bundled-types/host-tests/helpers/cards/view-card-demo.ts +119 -0
- package/bundled-types/host-tests/helpers/field-test-helpers.gts +54 -0
- package/bundled-types/host-tests/helpers/image-fixture.ts +3 -0
- package/bundled-types/host-tests/helpers/index.gts +2099 -0
- package/bundled-types/host-tests/helpers/indexer.ts +293 -0
- package/bundled-types/host-tests/helpers/interact-submode-setup.gts +551 -0
- package/bundled-types/host-tests/helpers/mock-matrix/_client.ts +1042 -0
- package/bundled-types/host-tests/helpers/mock-matrix/_sdk.ts +71 -0
- package/bundled-types/host-tests/helpers/mock-matrix/_server-state.ts +343 -0
- package/bundled-types/host-tests/helpers/mock-matrix/_sliding-sync.ts +140 -0
- package/bundled-types/host-tests/helpers/mock-matrix/_utils.ts +146 -0
- package/bundled-types/host-tests/helpers/mock-matrix.ts +200 -0
- package/bundled-types/host-tests/helpers/operator-mode-parameters-match.ts +71 -0
- package/bundled-types/host-tests/helpers/operator-mode-state.ts +18 -0
- package/bundled-types/host-tests/helpers/percy-snapshot.ts +228 -0
- package/bundled-types/host-tests/helpers/playground.ts +130 -0
- package/bundled-types/host-tests/helpers/realm-server-mock/index.ts +100 -0
- package/bundled-types/host-tests/helpers/realm-server-mock/routes.ts +617 -0
- package/bundled-types/host-tests/helpers/realm-server-mock/types.ts +24 -0
- package/bundled-types/host-tests/helpers/recent-files-cards.ts +86 -0
- package/bundled-types/host-tests/helpers/render-component.ts +40 -0
- package/bundled-types/host-tests/helpers/setup-qunit.js +164 -0
- package/bundled-types/host-tests/helpers/setup.ts +349 -0
- package/bundled-types/host-tests/helpers/shard-warmup.ts +72 -0
- package/bundled-types/host-tests/helpers/stream.ts +60 -0
- package/bundled-types/host-tests/helpers/test-auth.ts +27 -0
- package/bundled-types/host-tests/helpers/test-realm-registry.ts +23 -0
- package/bundled-types/host-tests/helpers/test-realm-service-worker.ts +92 -0
- package/bundled-types/host-tests/helpers/uncaught-exceptions.ts +16 -0
- package/bundled-types/host-tests/helpers/visit-operator-mode.ts +39 -0
- package/bundled-types/host-types/@cardstack/host/index.d.ts +3 -0
- package/bundled-types/host-types/@joplin/turndown-plugin-gfm/index.d.ts +15 -0
- package/bundled-types/host-types/@sqlite.org/sqlite-wasm/index.d.ts +122 -0
- package/bundled-types/host-types/ember-click-outside/modifiers/on-click-outside.ts +16 -0
- package/bundled-types/host-types/ember-concurrency/helpers/perform.d.ts +3 -0
- package/bundled-types/host-types/ember-css-url/index.d.ts +15 -0
- package/bundled-types/host-types/ember-data/types/registries/model.d.ts +6 -0
- package/bundled-types/host-types/ember-elsewhere/from-elsewhere.d.ts +12 -0
- package/bundled-types/host-types/ember-elsewhere/to-elsewhere.d.ts +16 -0
- package/bundled-types/host-types/ember-focus-trap/index.d.ts +4 -0
- package/bundled-types/host-types/ember-keyboard/modifiers/on-key.d.ts +15 -0
- package/bundled-types/host-types/index.d.ts +6 -0
- package/bundled-types/local-types/eslint-js.d.ts +5 -0
- package/bundled-types/local-types/index.d.ts +115 -0
- package/bundled-types/local-types/marked-gfm-heading-id.d.ts +12 -0
- package/bundled-types/local-types/matrix-js-sdk/index.d.ts +11 -0
- package/bundled-types/local-types/package.json +13 -0
- package/bundled-types/shims/boxel-cli-shims.d.ts +10 -0
- package/dist/index.js +100 -100
- package/package.json +13 -7
- package/src/commands/parse.ts +277 -34
- package/src/commands/realm/publish.ts +10 -1
- package/src/commands/realm/unpublish.ts +2 -3
- package/src/lib/describe-fetch-error.ts +25 -0
|
@@ -0,0 +1,2540 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isDestroyed,
|
|
3
|
+
isDestroying,
|
|
4
|
+
registerDestructor,
|
|
5
|
+
} from '@ember/destroyable';
|
|
6
|
+
import type Owner from '@ember/owner';
|
|
7
|
+
import { getOwner } from '@ember/owner';
|
|
8
|
+
import Service, { service } from '@ember/service';
|
|
9
|
+
import { buildWaiter } from '@ember/test-waiters';
|
|
10
|
+
|
|
11
|
+
import { isTesting } from '@embroider/macros';
|
|
12
|
+
|
|
13
|
+
import { formatDistanceToNow } from 'date-fns';
|
|
14
|
+
import { task } from 'ember-concurrency';
|
|
15
|
+
|
|
16
|
+
import cloneDeep from 'lodash/cloneDeep';
|
|
17
|
+
import isEqual from 'lodash/isEqual';
|
|
18
|
+
import merge from 'lodash/merge';
|
|
19
|
+
|
|
20
|
+
import { TrackedObject, TrackedMap } from 'tracked-built-ins';
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
baseFileRef,
|
|
24
|
+
CardError,
|
|
25
|
+
cardIdToURL,
|
|
26
|
+
isRegisteredPrefix,
|
|
27
|
+
hasExecutableExtension,
|
|
28
|
+
isCardError,
|
|
29
|
+
isCardInstance,
|
|
30
|
+
isFileDefInstance,
|
|
31
|
+
isFileMetaResource,
|
|
32
|
+
isSingleCardDocument,
|
|
33
|
+
isSingleFileMetaDocument,
|
|
34
|
+
isLinkableCollectionDocument,
|
|
35
|
+
resolveFileDefCodeRef,
|
|
36
|
+
X_BOXEL_JOB_PRIORITY_HEADER,
|
|
37
|
+
userInitiatedPriority,
|
|
38
|
+
Deferred,
|
|
39
|
+
delay,
|
|
40
|
+
mergeRelationships,
|
|
41
|
+
isLocalId,
|
|
42
|
+
realmURL as realmURLSymbol,
|
|
43
|
+
localId as localIdSymbol,
|
|
44
|
+
meta,
|
|
45
|
+
rri,
|
|
46
|
+
logger,
|
|
47
|
+
formattedError,
|
|
48
|
+
SupportedMimeType,
|
|
49
|
+
RealmPaths,
|
|
50
|
+
type Store as StoreInterface,
|
|
51
|
+
type AddOptions,
|
|
52
|
+
type CreateOptions,
|
|
53
|
+
type Query,
|
|
54
|
+
type DataQuery,
|
|
55
|
+
type QueryResultsMeta,
|
|
56
|
+
type RuntimeDependencyTrackingContext,
|
|
57
|
+
type PatchData,
|
|
58
|
+
type Relationship,
|
|
59
|
+
type AutoSaveState,
|
|
60
|
+
type CardDocument,
|
|
61
|
+
type SingleCardDocument,
|
|
62
|
+
type SingleFileMetaDocument,
|
|
63
|
+
type CardResourceMeta,
|
|
64
|
+
type LooseSingleCardDocument,
|
|
65
|
+
type LooseCardResource,
|
|
66
|
+
type CardErrorJSONAPI,
|
|
67
|
+
type CardErrorsJSONAPI,
|
|
68
|
+
type ErrorEntry,
|
|
69
|
+
type RenderError,
|
|
70
|
+
type FileMetaResource,
|
|
71
|
+
type LooseLinkableResource,
|
|
72
|
+
type LooseSingleResourceDocument,
|
|
73
|
+
type StoreReadType,
|
|
74
|
+
type CardResource,
|
|
75
|
+
type LinkableCollectionDocument,
|
|
76
|
+
type RealmIdentifier,
|
|
77
|
+
type RealmResourceIdentifier,
|
|
78
|
+
type Saved,
|
|
79
|
+
resolveCardReference,
|
|
80
|
+
} from '@cardstack/runtime-common';
|
|
81
|
+
|
|
82
|
+
import type { CardDef, BaseDef } from 'https://cardstack.com/base/card-api';
|
|
83
|
+
import type * as CardAPI from 'https://cardstack.com/base/card-api';
|
|
84
|
+
import type { FileDef } from 'https://cardstack.com/base/file-api';
|
|
85
|
+
|
|
86
|
+
import type { RealmEventContent } from 'https://cardstack.com/base/matrix-event';
|
|
87
|
+
|
|
88
|
+
import CardStore, { getDeps, type ReferenceCount } from '../lib/gc-card-store';
|
|
89
|
+
|
|
90
|
+
import {
|
|
91
|
+
consumingRealmHeader,
|
|
92
|
+
duringPrerenderHeaders,
|
|
93
|
+
jobIdHeader,
|
|
94
|
+
} from '../lib/prerender-fetch-headers';
|
|
95
|
+
import { searchCacheKey } from '../lib/search-cache-key';
|
|
96
|
+
import { searchInFlightKey } from '../lib/search-in-flight-key';
|
|
97
|
+
import { errorJsonApiToErrorEntry } from '../lib/window-error-handler';
|
|
98
|
+
import { getSearch } from '../resources/search';
|
|
99
|
+
import {
|
|
100
|
+
getSearchData,
|
|
101
|
+
type SearchDataResource,
|
|
102
|
+
} from '../resources/search-data';
|
|
103
|
+
|
|
104
|
+
import { FileDefAttributesExtractor } from '../utils/file-def-attributes-extractor';
|
|
105
|
+
import {
|
|
106
|
+
enableRenderTimerStub,
|
|
107
|
+
withTimersBlocked,
|
|
108
|
+
} from '../utils/render-timer-stub';
|
|
109
|
+
|
|
110
|
+
import type { CardSaveSubscriber } from './card-service';
|
|
111
|
+
import type CardService from './card-service';
|
|
112
|
+
import type CommandService from './command-service';
|
|
113
|
+
import type EnvironmentService from './environment-service';
|
|
114
|
+
|
|
115
|
+
import type HostModeService from './host-mode-service';
|
|
116
|
+
import type LoaderService from './loader-service';
|
|
117
|
+
import type MessageService from './message-service';
|
|
118
|
+
import type NetworkService from './network';
|
|
119
|
+
import type OperatorModeStateService from './operator-mode-state-service';
|
|
120
|
+
import type RealmService from './realm';
|
|
121
|
+
import type RealmServerService from './realm-server';
|
|
122
|
+
import type ResetService from './reset';
|
|
123
|
+
import type { SearchResource } from '../resources/search';
|
|
124
|
+
|
|
125
|
+
export { CardErrorJSONAPI, CardSaveSubscriber };
|
|
126
|
+
|
|
127
|
+
let waiter = buildWaiter('store-service');
|
|
128
|
+
|
|
129
|
+
const realmEventsLogger = logger('realm:events');
|
|
130
|
+
const storeLogger = logger('store');
|
|
131
|
+
|
|
132
|
+
// Companion to `jobIdHeader()` (re-exported from
|
|
133
|
+
// `../lib/prerender-fetch-headers`). Policy is two-state, gated by
|
|
134
|
+
// `__boxelDuringPrerender`, not by the presence of
|
|
135
|
+
// `__boxelJobPriority`:
|
|
136
|
+
//
|
|
137
|
+
// 1. Inside a prerender tab: forward the worker job's priority as-is.
|
|
138
|
+
// The render-runner injects `__boxelJobPriority` alongside
|
|
139
|
+
// `__boxelJobId` on each visit — a priority of 0 is meaningful
|
|
140
|
+
// (the originating job is system-initiated background indexing)
|
|
141
|
+
// and must be preserved, not upgraded. Sub-`prerenderModule`
|
|
142
|
+
// calls fired by `_federated-search` for a `lookupDefinition`
|
|
143
|
+
// cache miss inherit this priority so they don't outrun the
|
|
144
|
+
// parent. If `__boxelJobPriority` is missing here (older
|
|
145
|
+
// render-runner build, test fixture, etc.) treat as 0 — the
|
|
146
|
+
// safe default for prerender-context work.
|
|
147
|
+
//
|
|
148
|
+
// 2. Outside a prerender tab (the host SPA in a real user's browser):
|
|
149
|
+
// stamp `userInitiatedPriority` (10). User clicks driving a
|
|
150
|
+
// search are by definition user-initiated work and should outrank
|
|
151
|
+
// background indexing on the realm-server's PagePool. Without
|
|
152
|
+
// this, a user search whose definition lookup misses the modules
|
|
153
|
+
// cache would fire its sub-prerender at priority 0 and queue
|
|
154
|
+
// behind concurrent indexing fan-out.
|
|
155
|
+
//
|
|
156
|
+
// External (non-host) HTTP callers — anything that doesn't run in
|
|
157
|
+
// the host SPA's JS runtime — bypass this helper entirely and set
|
|
158
|
+
// `X-Boxel-Job-Priority` directly on their request if they care.
|
|
159
|
+
// This helper covers the host SPA only.
|
|
160
|
+
//
|
|
161
|
+
// Both globals are checked with `=== true` / strict-number rather
|
|
162
|
+
// than truthy coercion: `__boxelDuringPrerender` is typed as a
|
|
163
|
+
// boolean and a stray truthy string from a future code path
|
|
164
|
+
// shouldn't silently flip the policy from "user-priority" to
|
|
165
|
+
// "preserve 0."
|
|
166
|
+
// Pure resolver — exported for the unit test in
|
|
167
|
+
// `tests/integration/job-priority-header-test.ts`. See the comment
|
|
168
|
+
// above for the policy rationale; the function is the literal
|
|
169
|
+
// translation of that policy to numbers.
|
|
170
|
+
export function resolveOutboundJobPriority({
|
|
171
|
+
duringPrerender,
|
|
172
|
+
jobPriority,
|
|
173
|
+
}: {
|
|
174
|
+
duringPrerender: unknown;
|
|
175
|
+
jobPriority: unknown;
|
|
176
|
+
}): number {
|
|
177
|
+
let valid =
|
|
178
|
+
typeof jobPriority === 'number' &&
|
|
179
|
+
Number.isSafeInteger(jobPriority) &&
|
|
180
|
+
jobPriority >= 0
|
|
181
|
+
? jobPriority
|
|
182
|
+
: undefined;
|
|
183
|
+
if (duringPrerender === true) {
|
|
184
|
+
return valid ?? 0;
|
|
185
|
+
}
|
|
186
|
+
return valid ?? userInitiatedPriority;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function jobPriorityHeader(): Record<string, string> {
|
|
190
|
+
let g = globalThis as unknown as {
|
|
191
|
+
__boxelDuringPrerender?: boolean;
|
|
192
|
+
__boxelJobPriority?: number;
|
|
193
|
+
};
|
|
194
|
+
return {
|
|
195
|
+
[X_BOXEL_JOB_PRIORITY_HEADER]: String(
|
|
196
|
+
resolveOutboundJobPriority({
|
|
197
|
+
duringPrerender: g.__boxelDuringPrerender,
|
|
198
|
+
jobPriority: g.__boxelJobPriority,
|
|
199
|
+
}),
|
|
200
|
+
),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
const queryFieldSeedFromSearchSymbol = Symbol.for(
|
|
204
|
+
'cardstack-query-field-seed-from-search',
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
type PersistOptions = CreateOptions & { clientRequestId?: string };
|
|
208
|
+
type DependencyTrackingOptions = {
|
|
209
|
+
dependencyTrackingContext?: RuntimeDependencyTrackingContext;
|
|
210
|
+
};
|
|
211
|
+
type TrackedCreateOptions = CreateOptions & DependencyTrackingOptions;
|
|
212
|
+
type TrackedAddOptions = AddOptions & DependencyTrackingOptions;
|
|
213
|
+
|
|
214
|
+
export default class StoreService extends Service implements StoreInterface {
|
|
215
|
+
@service declare private realm: RealmService;
|
|
216
|
+
@service declare private loaderService: LoaderService;
|
|
217
|
+
@service declare private messageService: MessageService;
|
|
218
|
+
@service declare private cardService: CardService;
|
|
219
|
+
@service declare private commandService: CommandService;
|
|
220
|
+
@service declare private hostModeService: HostModeService;
|
|
221
|
+
@service declare private network: NetworkService;
|
|
222
|
+
@service declare private environmentService: EnvironmentService;
|
|
223
|
+
@service declare private reset: ResetService;
|
|
224
|
+
@service declare private operatorModeStateService: OperatorModeStateService;
|
|
225
|
+
@service declare private realmServer: RealmServerService;
|
|
226
|
+
private subscriptions: Map<string, { unsubscribe: () => void }> = new Map();
|
|
227
|
+
private referenceCount: ReferenceCount = new Map();
|
|
228
|
+
private newReferencePromises: Promise<void>[] = [];
|
|
229
|
+
private autoSaveStates: TrackedMap<string, AutoSaveState> = new TrackedMap();
|
|
230
|
+
private cardApiCache?: typeof CardAPI;
|
|
231
|
+
private gcInterval: number | undefined;
|
|
232
|
+
private ready: Promise<void>;
|
|
233
|
+
private inflightGetCards: Map<string, Promise<CardDef | CardErrorJSONAPI>> =
|
|
234
|
+
new Map();
|
|
235
|
+
private inflightGetFileMeta: Map<
|
|
236
|
+
string,
|
|
237
|
+
Promise<FileDef | CardErrorJSONAPI>
|
|
238
|
+
> = new Map();
|
|
239
|
+
private inflightCardMutations: Map<string, Promise<void>> = new Map();
|
|
240
|
+
private inflightCardLoads: Map<string, Deferred<void>> = new Map();
|
|
241
|
+
// Coalesce concurrent same-(realms, query) `_federated-search` HTTP
|
|
242
|
+
// calls during a prerender. Mirrors
|
|
243
|
+
// `RealmIndexQueryEngine.#inFlightSearch` server-side. Gated on
|
|
244
|
+
// `__boxelRenderContext` so live user searches stay uncoalesced —
|
|
245
|
+
// write-then-read freshness story unchanged outside prerender.
|
|
246
|
+
// Entries self-clear on `.finally()` via identity check.
|
|
247
|
+
private inflightSearch: Map<string, Promise<LinkableCollectionDocument>> =
|
|
248
|
+
new Map();
|
|
249
|
+
// Resolved-doc cache for same-realm `_federated-search` calls during
|
|
250
|
+
// a prerender. Layered *above* `inflightSearch`: a cache hit skips
|
|
251
|
+
// the network round-trip entirely; a miss falls through to the
|
|
252
|
+
// in-flight Map and the cache is populated on resolve. Keyed by
|
|
253
|
+
// (jobId, consumingRealm, query) — gated to same-realm-only so a
|
|
254
|
+
// cross-realm read can't freeze a value while a peer realm-server
|
|
255
|
+
// replica swaps mid-job.
|
|
256
|
+
//
|
|
257
|
+
// Lifetime: the entire indexing job. One job typically spans many
|
|
258
|
+
// card renders in the same prerender tab (each navigation activates
|
|
259
|
+
// and deactivates the render route but all those visits share one
|
|
260
|
+
// `__boxelJobId`); the cache must survive those route bounces so
|
|
261
|
+
// earlier renders' work is reusable by later ones. Only clear when
|
|
262
|
+
// the job actually changes — `fetchSearchDoc` does this at
|
|
263
|
+
// fetch-entry via the jobId-change check, and `resetState` /
|
|
264
|
+
// `resetCache` do it on harder service resets. The render route's
|
|
265
|
+
// `deactivate` deliberately does NOT clear this cache. See
|
|
266
|
+
// `search-cache-key.ts` for the digest and the realm-server's
|
|
267
|
+
// `job-scoped-search-cache.ts` for the server-side prior art on
|
|
268
|
+
// storing resolved docs rather than promises (avoids tail-latency
|
|
269
|
+
// stalls on slow first populate).
|
|
270
|
+
private searchCache: Map<string, LinkableCollectionDocument> = new Map();
|
|
271
|
+
// The jobId the `searchCache` entries belong to. When a request
|
|
272
|
+
// arrives carrying a different `__boxelJobId` we drop the cache
|
|
273
|
+
// before serving — belt-and-braces beside `resetState()` and the
|
|
274
|
+
// render-route deactivate clear, in case a prerender tab is reused
|
|
275
|
+
// across jobs without driving either of those paths.
|
|
276
|
+
private searchCacheJobId: string | undefined = undefined;
|
|
277
|
+
// Monotonic counter bumped on every clear of `searchCache` (every
|
|
278
|
+
// path that empties the map: `clearSearchCache`, `resetState`,
|
|
279
|
+
// `resetCache`, the jobId-change clear at fetch-entry). A
|
|
280
|
+
// `fetchSearchDoc` call captures this at entry and checks it before
|
|
281
|
+
// populating on resolve — if the cache was intentionally cleared
|
|
282
|
+
// while the request was in flight, the resolved doc must not
|
|
283
|
+
// repopulate against the new generation. Mirrors the identity
|
|
284
|
+
// check on the in-flight Map but for the resolved-doc layer where
|
|
285
|
+
// we can't compare against a stored Promise.
|
|
286
|
+
private searchCacheGeneration = 0;
|
|
287
|
+
private store: CardStore;
|
|
288
|
+
protected isRenderStore = false;
|
|
289
|
+
|
|
290
|
+
// This is used for tests
|
|
291
|
+
private onSaveSubscriber: CardSaveSubscriber | undefined;
|
|
292
|
+
private autoSaveQueues = new Map<string, { isImmediate?: true }[]>();
|
|
293
|
+
private autoSavePromises = new Map<string, Promise<void>>();
|
|
294
|
+
|
|
295
|
+
constructor(owner: Owner) {
|
|
296
|
+
super(owner);
|
|
297
|
+
this.store = this.createCardStore();
|
|
298
|
+
this.reset.register(this);
|
|
299
|
+
this.ready = this.setup();
|
|
300
|
+
registerDestructor(this, () => {
|
|
301
|
+
clearInterval(this.gcInterval);
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
protected renderContextBlocksPersistence() {
|
|
306
|
+
return (
|
|
307
|
+
this.isRenderStore && Boolean((globalThis as any).__boxelRenderContext)
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// used for tests only!
|
|
312
|
+
_onSave(subscriber: CardSaveSubscriber) {
|
|
313
|
+
this.onSaveSubscriber = subscriber;
|
|
314
|
+
this.cardService._onSave(subscriber);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// used for tests only!
|
|
318
|
+
_unregisterSaveSubscriber() {
|
|
319
|
+
this.onSaveSubscriber = undefined;
|
|
320
|
+
this.cardService._unregisterSaveSubscriber();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
resetState() {
|
|
324
|
+
clearInterval(this.gcInterval);
|
|
325
|
+
this.subscriptions = new Map();
|
|
326
|
+
this.onSaveSubscriber = undefined;
|
|
327
|
+
this.referenceCount = new Map();
|
|
328
|
+
this.newReferencePromises = [];
|
|
329
|
+
this.autoSaveStates = new TrackedMap();
|
|
330
|
+
this.inflightGetCards = new Map();
|
|
331
|
+
this.inflightGetFileMeta = new Map();
|
|
332
|
+
this.inflightCardMutations = new Map();
|
|
333
|
+
this.inflightCardLoads = new Map();
|
|
334
|
+
this.inflightSearch = new Map();
|
|
335
|
+
this.searchCache = new Map();
|
|
336
|
+
this.searchCacheJobId = undefined;
|
|
337
|
+
this.searchCacheGeneration++;
|
|
338
|
+
this.autoSaveQueues = new Map();
|
|
339
|
+
this.autoSavePromises = new Map();
|
|
340
|
+
this.store = this.createCardStore();
|
|
341
|
+
this.ready = this.setup();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async ensureSetupComplete(): Promise<void> {
|
|
345
|
+
await this.ready;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Drop every pending in-flight search entry. Callers awaiting an
|
|
349
|
+
// existing promise still get their answer (the underlying HTTP is
|
|
350
|
+
// already in motion); only *new* same-key callers after the drop
|
|
351
|
+
// miss the map and re-fetch. Wire this to anything the host
|
|
352
|
+
// recognizes as an invalidation boundary — render-route deactivate
|
|
353
|
+
// is the obvious one inside a prerender tab.
|
|
354
|
+
clearInFlightSearch(): void {
|
|
355
|
+
this.inflightSearch.clear();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Drop every resolved-doc search-cache entry. Used for hard resets
|
|
359
|
+
// (`resetState`, `resetCache`) and by tests; NOT called from the
|
|
360
|
+
// render route's per-visit deactivate, because the cache is meant
|
|
361
|
+
// to survive across renders within a single indexing job. Cross-job
|
|
362
|
+
// invalidation is handled by `fetchSearchDoc`'s entry-time
|
|
363
|
+
// jobId-change clear, which fires the first time a new
|
|
364
|
+
// `__boxelJobId` is observed.
|
|
365
|
+
clearSearchCache(): void {
|
|
366
|
+
this.searchCache.clear();
|
|
367
|
+
this.searchCacheJobId = undefined;
|
|
368
|
+
this.searchCacheGeneration++;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
resetCache(opts?: { preserveReferences?: boolean }) {
|
|
372
|
+
storeLogger.debug('resetting store cache');
|
|
373
|
+
if (!opts?.preserveReferences) {
|
|
374
|
+
this.referenceCount = new Map();
|
|
375
|
+
}
|
|
376
|
+
this.cardApiCache = undefined;
|
|
377
|
+
this.autoSaveStates = new TrackedMap();
|
|
378
|
+
this.newReferencePromises = [];
|
|
379
|
+
this.inflightGetCards = new Map();
|
|
380
|
+
this.inflightGetFileMeta = new Map();
|
|
381
|
+
this.inflightCardMutations = new Map();
|
|
382
|
+
this.inflightCardLoads = new Map();
|
|
383
|
+
this.inflightSearch = new Map();
|
|
384
|
+
this.searchCache = new Map();
|
|
385
|
+
this.searchCacheJobId = undefined;
|
|
386
|
+
this.searchCacheGeneration++;
|
|
387
|
+
this.autoSaveQueues = new Map();
|
|
388
|
+
this.autoSavePromises = new Map();
|
|
389
|
+
this.store = this.createCardStore();
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
refreshReferencesForCodeChange(reason?: string) {
|
|
393
|
+
let reasonSuffix = reason ? ` (${reason})` : '';
|
|
394
|
+
storeLogger.debug(`resetting store for code change${reasonSuffix}`);
|
|
395
|
+
this.store.reset();
|
|
396
|
+
this.reestablishReferences.perform();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
dropReference(id: string | undefined) {
|
|
400
|
+
if (!id) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
id = asURL(id);
|
|
404
|
+
let currentReferenceCount = this.referenceCount.get(id) ?? 0;
|
|
405
|
+
currentReferenceCount -= 1;
|
|
406
|
+
this.referenceCount.set(id, currentReferenceCount);
|
|
407
|
+
|
|
408
|
+
storeLogger.debug(
|
|
409
|
+
`dropping reference to ${id}, current reference count: ${this.referenceCount.get(id)}`,
|
|
410
|
+
);
|
|
411
|
+
if (currentReferenceCount <= 0) {
|
|
412
|
+
if (currentReferenceCount < 0) {
|
|
413
|
+
let message = `current reference count for ${id} is negative: ${this.referenceCount.get(id)}`;
|
|
414
|
+
storeLogger.error(message);
|
|
415
|
+
console.trace(message); // this will helps us to understand who dropped the reference that made it negative
|
|
416
|
+
}
|
|
417
|
+
this.referenceCount.delete(id);
|
|
418
|
+
this.autoSaveStates.delete(id);
|
|
419
|
+
this.unsubscribeFromInstance(id);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
addReference(id: string | undefined, opts?: { type?: StoreReadType }) {
|
|
424
|
+
if (!id) {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
id = asURL(id);
|
|
428
|
+
let readType: StoreReadType = opts?.type ?? 'card';
|
|
429
|
+
// synchronously update the reference count so we don't run into race
|
|
430
|
+
// conditions requiring a mutex
|
|
431
|
+
let currentReferenceCount = this.referenceCount.get(id) ?? 0;
|
|
432
|
+
currentReferenceCount += 1;
|
|
433
|
+
this.referenceCount.set(id, currentReferenceCount);
|
|
434
|
+
storeLogger.debug(
|
|
435
|
+
`adding reference to ${id}, current reference count: ${this.referenceCount.get(id)}`,
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
if (isLocalId(id)) {
|
|
439
|
+
let instanceOrError = this.peek(id);
|
|
440
|
+
if (instanceOrError) {
|
|
441
|
+
let realmURL = isCardInstance(instanceOrError)
|
|
442
|
+
? instanceOrError[realmURLSymbol]?.href
|
|
443
|
+
: instanceOrError.realm;
|
|
444
|
+
if (realmURL) {
|
|
445
|
+
this.subscribeToRealm(new URL(realmURL));
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
} else {
|
|
449
|
+
this.subscribeToRealm(rri(id));
|
|
450
|
+
// intentionally not awaiting this. we keep track of the promise in
|
|
451
|
+
// this.newReferencePromises
|
|
452
|
+
this.wireUpNewReference(id, readType);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
loaded(): Promise<void> {
|
|
457
|
+
return this.store.loaded();
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
get loadGeneration(): number {
|
|
461
|
+
return this.store.loadGeneration;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
trackLoad(load: Promise<unknown>): void {
|
|
465
|
+
this.store.trackLoad(load);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// CS-10872: pass-through so SearchResource / other callers can tag
|
|
469
|
+
// their load promises with the metadata we want to see in a timeout
|
|
470
|
+
// error document ("what query fields were still pending").
|
|
471
|
+
trackQueryLoad(
|
|
472
|
+
load: Promise<unknown>,
|
|
473
|
+
meta: import('https://cardstack.com/base/card-api').QueryLoadMeta,
|
|
474
|
+
): (() => void) | void {
|
|
475
|
+
return (
|
|
476
|
+
this.store as unknown as {
|
|
477
|
+
trackQueryLoad?: (
|
|
478
|
+
l: Promise<unknown>,
|
|
479
|
+
m: import('https://cardstack.com/base/card-api').QueryLoadMeta,
|
|
480
|
+
) => (() => void) | void;
|
|
481
|
+
}
|
|
482
|
+
).trackQueryLoad?.(load, meta);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
queryLoadsInFlight(): import('https://cardstack.com/base/card-api').QueryLoadInfo[] {
|
|
486
|
+
return (
|
|
487
|
+
(
|
|
488
|
+
this.store as unknown as {
|
|
489
|
+
queryLoadsInFlight?: () => import('https://cardstack.com/base/card-api').QueryLoadInfo[];
|
|
490
|
+
}
|
|
491
|
+
).queryLoadsInFlight?.() ?? []
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// CS-10872: pass-throughs for the per-item diagnostic accessors.
|
|
496
|
+
// Each returns [] when the underlying store doesn't implement the
|
|
497
|
+
// hook (older test doubles, in-memory stores in node-side tests).
|
|
498
|
+
cardDocLoadsInFlight(): Array<{ url: string; ageMs: number }> {
|
|
499
|
+
return (
|
|
500
|
+
(
|
|
501
|
+
this.store as unknown as {
|
|
502
|
+
cardDocLoadsInFlight?: () => Array<{ url: string; ageMs: number }>;
|
|
503
|
+
}
|
|
504
|
+
).cardDocLoadsInFlight?.() ?? []
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
fileMetaDocLoadsInFlight(): Array<{ url: string; ageMs: number }> {
|
|
508
|
+
return (
|
|
509
|
+
(
|
|
510
|
+
this.store as unknown as {
|
|
511
|
+
fileMetaDocLoadsInFlight?: () => Array<{
|
|
512
|
+
url: string;
|
|
513
|
+
ageMs: number;
|
|
514
|
+
}>;
|
|
515
|
+
}
|
|
516
|
+
).fileMetaDocLoadsInFlight?.() ?? []
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
recentCardDocLoads(): Array<{ url: string; ms: number }> {
|
|
520
|
+
return (
|
|
521
|
+
(
|
|
522
|
+
this.store as unknown as {
|
|
523
|
+
recentCardDocLoads?: () => Array<{ url: string; ms: number }>;
|
|
524
|
+
}
|
|
525
|
+
).recentCardDocLoads?.() ?? []
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
recentFileMetaLoads(): Array<{ url: string; ms: number }> {
|
|
529
|
+
return (
|
|
530
|
+
(
|
|
531
|
+
this.store as unknown as {
|
|
532
|
+
recentFileMetaLoads?: () => Array<{ url: string; ms: number }>;
|
|
533
|
+
}
|
|
534
|
+
).recentFileMetaLoads?.() ?? []
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
recentQueryLoads(): Array<{
|
|
538
|
+
meta: import('https://cardstack.com/base/card-api').QueryLoadMeta;
|
|
539
|
+
ms: number;
|
|
540
|
+
}> {
|
|
541
|
+
return (
|
|
542
|
+
(
|
|
543
|
+
this.store as unknown as {
|
|
544
|
+
recentQueryLoads?: () => Array<{
|
|
545
|
+
meta: import('https://cardstack.com/base/card-api').QueryLoadMeta;
|
|
546
|
+
ms: number;
|
|
547
|
+
}>;
|
|
548
|
+
}
|
|
549
|
+
).recentQueryLoads?.() ?? []
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
get cardDocsInFlight() {
|
|
554
|
+
return this.store.cardDocsInFlight;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
get fileMetaDocsInFlight() {
|
|
558
|
+
return this.store.fileMetaDocsInFlight;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// This method creates a new instance in the store and return the new card ID
|
|
562
|
+
async create(
|
|
563
|
+
doc: LooseSingleCardDocument,
|
|
564
|
+
opts?: TrackedCreateOptions,
|
|
565
|
+
): Promise<string | CardErrorJSONAPI> {
|
|
566
|
+
return await this.withTestWaiters(async () => {
|
|
567
|
+
if (opts?.realm) {
|
|
568
|
+
doc.data.meta = {
|
|
569
|
+
...(doc.data.meta ?? {}),
|
|
570
|
+
realmURL: opts.realm as RealmIdentifier,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
let cardOrError = await this.getCardInstance({
|
|
574
|
+
idOrDoc: doc,
|
|
575
|
+
relativeTo: opts?.relativeTo,
|
|
576
|
+
realm: opts?.realm,
|
|
577
|
+
opts: {
|
|
578
|
+
localDir: opts?.localDir,
|
|
579
|
+
dependencyTrackingContext: opts?.dependencyTrackingContext,
|
|
580
|
+
},
|
|
581
|
+
});
|
|
582
|
+
if (isCardInstance(cardOrError)) {
|
|
583
|
+
return cardOrError.id;
|
|
584
|
+
}
|
|
585
|
+
return cardOrError;
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
save(id: string) {
|
|
590
|
+
this.doAutoSave(id, { isImmediate: true });
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
async add<T extends CardDef>(
|
|
594
|
+
instanceOrDoc: T | LooseSingleCardDocument,
|
|
595
|
+
opts?: TrackedCreateOptions & { doNotPersist: true },
|
|
596
|
+
): Promise<T>;
|
|
597
|
+
async add<T extends CardDef>(
|
|
598
|
+
instanceOrDoc: T | LooseSingleCardDocument,
|
|
599
|
+
opts?: TrackedCreateOptions & { doNotWaitForPersist: true },
|
|
600
|
+
): Promise<T>;
|
|
601
|
+
async add<T extends CardDef>(
|
|
602
|
+
instanceOrDoc: T | LooseSingleCardDocument,
|
|
603
|
+
opts?: TrackedCreateOptions,
|
|
604
|
+
): Promise<T | CardErrorJSONAPI>;
|
|
605
|
+
async add<T extends CardDef>(
|
|
606
|
+
instanceOrDoc: T | LooseSingleCardDocument,
|
|
607
|
+
opts?: TrackedAddOptions,
|
|
608
|
+
): Promise<T | CardErrorJSONAPI> {
|
|
609
|
+
let instance: T;
|
|
610
|
+
if (!isCardInstance(instanceOrDoc)) {
|
|
611
|
+
instance = await this.createFromSerialized(
|
|
612
|
+
instanceOrDoc.data,
|
|
613
|
+
instanceOrDoc,
|
|
614
|
+
opts?.relativeTo,
|
|
615
|
+
opts?.dependencyTrackingContext,
|
|
616
|
+
);
|
|
617
|
+
} else {
|
|
618
|
+
instance = instanceOrDoc;
|
|
619
|
+
let api = await this.cardService.getAPI();
|
|
620
|
+
let deps = getDeps(api, instance);
|
|
621
|
+
for (let dep of deps) {
|
|
622
|
+
if (isCardInstance(dep)) {
|
|
623
|
+
if (!this.store.getCard(dep[localIdSymbol])) {
|
|
624
|
+
this.store.setCard(dep.id ?? dep[localIdSymbol], dep);
|
|
625
|
+
}
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
if (isFileDefInstance(dep) && dep.id) {
|
|
629
|
+
if (!this.store.getFileMeta(dep.id)) {
|
|
630
|
+
this.store.setFileMeta(dep.id, dep);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
if (opts?.realm) {
|
|
636
|
+
instance[meta] = {
|
|
637
|
+
...instance[meta],
|
|
638
|
+
...{ realmURL: opts.realm },
|
|
639
|
+
} as CardResourceMeta;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
let maybeOldInstance = instance.id
|
|
643
|
+
? this.store.getCard(instance.id)
|
|
644
|
+
: undefined;
|
|
645
|
+
if (maybeOldInstance) {
|
|
646
|
+
await this.stopAutoSaving(maybeOldInstance);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
this.setIdentityContext(instance);
|
|
650
|
+
await this.startAutoSaving(instance);
|
|
651
|
+
|
|
652
|
+
if (this.renderContextBlocksPersistence()) {
|
|
653
|
+
return instance;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (opts?.doNotWaitForPersist) {
|
|
657
|
+
// intentionally not awaiting
|
|
658
|
+
this.persistAndUpdate(instance, {
|
|
659
|
+
realm: opts?.realm,
|
|
660
|
+
localDir: opts?.localDir,
|
|
661
|
+
});
|
|
662
|
+
} else if (!opts?.doNotPersist) {
|
|
663
|
+
if (instance.id) {
|
|
664
|
+
this.save(instance.id);
|
|
665
|
+
} else {
|
|
666
|
+
return (await this.persistAndUpdate(instance, {
|
|
667
|
+
realm: opts?.realm,
|
|
668
|
+
localDir: opts?.localDir,
|
|
669
|
+
})) as T | CardErrorJSONAPI;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
return instance;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// peek will return a stale instance in the case the server has an error for
|
|
677
|
+
// this id
|
|
678
|
+
peek<T extends CardDef>(
|
|
679
|
+
id: string,
|
|
680
|
+
opts?: { type?: 'card' },
|
|
681
|
+
): T | CardErrorJSONAPI | undefined;
|
|
682
|
+
peek<T extends FileDef>(
|
|
683
|
+
id: string,
|
|
684
|
+
opts: { type: 'file-meta' },
|
|
685
|
+
): T | CardErrorJSONAPI | undefined;
|
|
686
|
+
peek<T extends CardDef | FileDef>(
|
|
687
|
+
id: string,
|
|
688
|
+
opts?: { type?: StoreReadType },
|
|
689
|
+
): T | CardErrorJSONAPI | undefined {
|
|
690
|
+
id = asURL(id);
|
|
691
|
+
let readType = opts?.type ?? 'card';
|
|
692
|
+
if (readType === 'file-meta') {
|
|
693
|
+
return this.store.getFileMetaInstanceOrError<T & FileDef>(id);
|
|
694
|
+
}
|
|
695
|
+
return this.store.getCardInstanceOrError<T & CardDef>(id);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// peekError will always return the current server state regarding errors for this id
|
|
699
|
+
peekError(id: string, opts?: { type?: 'card' }): CardErrorJSONAPI | undefined;
|
|
700
|
+
peekError(
|
|
701
|
+
id: string,
|
|
702
|
+
opts: { type: 'file-meta' },
|
|
703
|
+
): CardErrorJSONAPI | undefined;
|
|
704
|
+
peekError(
|
|
705
|
+
id: string,
|
|
706
|
+
opts?: { type?: StoreReadType },
|
|
707
|
+
): CardErrorJSONAPI | undefined {
|
|
708
|
+
id = asURL(id);
|
|
709
|
+
let readType = opts?.type ?? 'card';
|
|
710
|
+
if (readType === 'file-meta') {
|
|
711
|
+
return this.store.getFileMetaError(id);
|
|
712
|
+
}
|
|
713
|
+
return this.store.getCardError(id);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
async get<T extends CardDef>(
|
|
717
|
+
id: string,
|
|
718
|
+
opts?: {
|
|
719
|
+
type?: 'card';
|
|
720
|
+
dependencyTrackingContext?: RuntimeDependencyTrackingContext;
|
|
721
|
+
},
|
|
722
|
+
): Promise<T | CardErrorJSONAPI>;
|
|
723
|
+
async get<T extends FileDef>(
|
|
724
|
+
id: string,
|
|
725
|
+
opts: {
|
|
726
|
+
type: 'file-meta';
|
|
727
|
+
dependencyTrackingContext?: RuntimeDependencyTrackingContext;
|
|
728
|
+
},
|
|
729
|
+
): Promise<T | CardErrorJSONAPI>;
|
|
730
|
+
async get<T extends CardDef | FileDef>(
|
|
731
|
+
id: string,
|
|
732
|
+
opts?: {
|
|
733
|
+
type?: StoreReadType;
|
|
734
|
+
dependencyTrackingContext?: RuntimeDependencyTrackingContext;
|
|
735
|
+
},
|
|
736
|
+
): Promise<T | CardErrorJSONAPI> {
|
|
737
|
+
let readType = opts?.type ?? 'card';
|
|
738
|
+
if (readType === 'file-meta') {
|
|
739
|
+
return await this.getFileMetaInstance<T & FileDef>({
|
|
740
|
+
idOrDoc: id,
|
|
741
|
+
opts: { dependencyTrackingContext: opts?.dependencyTrackingContext },
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
return await this.getCardInstance<T & CardDef>({
|
|
745
|
+
idOrDoc: id,
|
|
746
|
+
opts: { dependencyTrackingContext: opts?.dependencyTrackingContext },
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Bypass cached state and fetch from source of truth
|
|
751
|
+
async getWithoutCache<T extends CardDef>(
|
|
752
|
+
id: string,
|
|
753
|
+
opts?: { type?: 'card' },
|
|
754
|
+
): Promise<T | CardErrorJSONAPI>;
|
|
755
|
+
async getWithoutCache<T extends FileDef>(
|
|
756
|
+
id: string,
|
|
757
|
+
opts: { type: 'file-meta' },
|
|
758
|
+
): Promise<T | CardErrorJSONAPI>;
|
|
759
|
+
async getWithoutCache<T extends CardDef | FileDef>(
|
|
760
|
+
id: string,
|
|
761
|
+
opts?: { type?: StoreReadType },
|
|
762
|
+
): Promise<T | CardErrorJSONAPI> {
|
|
763
|
+
let readType = opts?.type ?? 'card';
|
|
764
|
+
if (readType === 'file-meta') {
|
|
765
|
+
return await this.getFileMetaInstance<T & FileDef>({
|
|
766
|
+
idOrDoc: id,
|
|
767
|
+
opts: { noCache: true },
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
return await this.getCardInstance<T & CardDef>({
|
|
771
|
+
idOrDoc: id,
|
|
772
|
+
opts: { noCache: true },
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
async serializeFileDefAsDocument(
|
|
777
|
+
fileDef: FileDef,
|
|
778
|
+
): Promise<SingleFileMetaDocument> {
|
|
779
|
+
let api = await this.cardService.getAPI();
|
|
780
|
+
return api.serializeFileDef(fileDef) as SingleFileMetaDocument;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
async delete(id: string): Promise<void> {
|
|
784
|
+
id = asURL(id);
|
|
785
|
+
if (!id) {
|
|
786
|
+
// the card isn't actually saved yet, so do nothing
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
this.unsubscribeFromInstance(id);
|
|
790
|
+
this.store.delete(id);
|
|
791
|
+
await this.cardService.fetchJSON(id, { method: 'DELETE' });
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
async patch<T extends CardDef = CardDef>(
|
|
795
|
+
id: string,
|
|
796
|
+
patch: PatchData,
|
|
797
|
+
opts?: { doNotPersist?: true },
|
|
798
|
+
): Promise<T | CardErrorJSONAPI | undefined>;
|
|
799
|
+
async patch<T extends CardDef = CardDef>(
|
|
800
|
+
id: string,
|
|
801
|
+
patch: PatchData,
|
|
802
|
+
opts?: { doNotWaitForPersist?: true },
|
|
803
|
+
): Promise<T | CardErrorJSONAPI | undefined>;
|
|
804
|
+
async patch<T extends CardDef = CardDef>(
|
|
805
|
+
id: string,
|
|
806
|
+
patch: PatchData,
|
|
807
|
+
opts?: { doNotPersist?: true; doNotWaitForPersist?: true },
|
|
808
|
+
): Promise<T | CardErrorJSONAPI | undefined>;
|
|
809
|
+
async patch<T extends CardDef = CardDef>(
|
|
810
|
+
id: string,
|
|
811
|
+
patch: PatchData,
|
|
812
|
+
opts?: { clientRequestId?: string },
|
|
813
|
+
): Promise<T | CardErrorJSONAPI | undefined>;
|
|
814
|
+
async patch<T extends CardDef = CardDef>(
|
|
815
|
+
id: string,
|
|
816
|
+
patch: PatchData,
|
|
817
|
+
opts?: { doNotWaitForPersist?: true; clientRequestId?: string },
|
|
818
|
+
): Promise<T | CardErrorJSONAPI | undefined>;
|
|
819
|
+
async patch<T extends CardDef = CardDef>(
|
|
820
|
+
id: string,
|
|
821
|
+
patch: PatchData,
|
|
822
|
+
opts?: {
|
|
823
|
+
doNotPersist?: true;
|
|
824
|
+
doNotWaitForPersist?: true;
|
|
825
|
+
clientRequestId?: string;
|
|
826
|
+
},
|
|
827
|
+
): Promise<T | CardErrorJSONAPI | undefined> {
|
|
828
|
+
if (this.renderContextBlocksPersistence()) {
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
// eslint-disable-next-line ember/classic-decorator-no-classic-methods
|
|
832
|
+
let instance = await this.get<T>(id);
|
|
833
|
+
if (!instance || !isCardInstance(instance)) {
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
if (opts?.doNotPersist) {
|
|
837
|
+
await this.stopAutoSaving(instance);
|
|
838
|
+
}
|
|
839
|
+
let doc = await this.cardService.serializeCard(instance, {
|
|
840
|
+
omitQueryFields: true,
|
|
841
|
+
});
|
|
842
|
+
if (patch.attributes) {
|
|
843
|
+
doc.data.attributes = merge(doc.data.attributes, patch.attributes);
|
|
844
|
+
}
|
|
845
|
+
if (patch.relationships) {
|
|
846
|
+
let mergedRel = mergeRelationships(
|
|
847
|
+
doc.data.relationships,
|
|
848
|
+
patch.relationships,
|
|
849
|
+
);
|
|
850
|
+
if (mergedRel && Object.keys(mergedRel).length !== 0) {
|
|
851
|
+
doc.data.relationships = mergedRel;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
if (patch.meta) {
|
|
855
|
+
doc.data.meta = merge(doc.data.meta, patch.meta);
|
|
856
|
+
}
|
|
857
|
+
let linkedCards = await this.loadPatchedInstances(
|
|
858
|
+
patch,
|
|
859
|
+
instance.id ? cardIdToURL(instance.id) : undefined,
|
|
860
|
+
);
|
|
861
|
+
for (let [field, value] of Object.entries(linkedCards)) {
|
|
862
|
+
if (field.includes('.')) {
|
|
863
|
+
let parts = field.split('.');
|
|
864
|
+
let leaf = parts.pop();
|
|
865
|
+
if (!leaf) {
|
|
866
|
+
throw new Error(`bug: error in field name "${field}"`);
|
|
867
|
+
}
|
|
868
|
+
let inner = instance;
|
|
869
|
+
for (let part of parts) {
|
|
870
|
+
inner = (inner as any)[part];
|
|
871
|
+
}
|
|
872
|
+
(inner as any)[leaf.match(/^\d+$/) ? Number(leaf) : leaf] = value;
|
|
873
|
+
} else {
|
|
874
|
+
(instance as any)[field] = value;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
let api = await this.cardService.getAPI();
|
|
878
|
+
await api.updateFromSerialized(instance, doc, this.store);
|
|
879
|
+
let shouldPersist = !opts?.doNotPersist;
|
|
880
|
+
let shouldAwaitPersist = shouldPersist && !opts?.doNotWaitForPersist;
|
|
881
|
+
let persistedResult: CardDef | CardErrorJSONAPI | undefined = instance;
|
|
882
|
+
|
|
883
|
+
if (opts?.doNotPersist) {
|
|
884
|
+
await this.startAutoSaving(instance);
|
|
885
|
+
} else if (shouldPersist) {
|
|
886
|
+
let persistPromise = this.persistAndUpdate(instance, {
|
|
887
|
+
clientRequestId: opts?.clientRequestId,
|
|
888
|
+
});
|
|
889
|
+
if (shouldAwaitPersist) {
|
|
890
|
+
persistedResult = await persistPromise;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
return persistedResult as T | CardErrorJSONAPI;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
async search(
|
|
898
|
+
query: DataQuery,
|
|
899
|
+
realms?: string[],
|
|
900
|
+
): Promise<(CardResource<Saved> | FileMetaResource)[]>;
|
|
901
|
+
async search(
|
|
902
|
+
query: DataQuery,
|
|
903
|
+
realms: string[] | undefined,
|
|
904
|
+
opts: {
|
|
905
|
+
includeMeta: true;
|
|
906
|
+
dependencyTrackingContext?: RuntimeDependencyTrackingContext;
|
|
907
|
+
},
|
|
908
|
+
): Promise<{
|
|
909
|
+
resources: (CardResource<Saved> | FileMetaResource)[];
|
|
910
|
+
meta: QueryResultsMeta;
|
|
911
|
+
}>;
|
|
912
|
+
async search<T extends CardDef | FileDef = CardDef>(
|
|
913
|
+
query: Query,
|
|
914
|
+
realms?: string[],
|
|
915
|
+
): Promise<T[]>;
|
|
916
|
+
async search<T extends CardDef | FileDef = CardDef>(
|
|
917
|
+
query: Query,
|
|
918
|
+
realms: string[] | undefined,
|
|
919
|
+
opts: {
|
|
920
|
+
includeMeta: true;
|
|
921
|
+
dependencyTrackingContext?: RuntimeDependencyTrackingContext;
|
|
922
|
+
},
|
|
923
|
+
): Promise<{ instances: T[]; meta: QueryResultsMeta }>;
|
|
924
|
+
async search<T extends CardDef | FileDef = CardDef>(
|
|
925
|
+
query: Query,
|
|
926
|
+
realms?: string[],
|
|
927
|
+
opts?: {
|
|
928
|
+
includeMeta?: boolean;
|
|
929
|
+
dependencyTrackingContext?: RuntimeDependencyTrackingContext;
|
|
930
|
+
},
|
|
931
|
+
): Promise<
|
|
932
|
+
| T[]
|
|
933
|
+
| (CardResource<Saved> | FileMetaResource)[]
|
|
934
|
+
| { instances: T[]; meta: QueryResultsMeta }
|
|
935
|
+
| {
|
|
936
|
+
resources: (CardResource<Saved> | FileMetaResource)[];
|
|
937
|
+
meta: QueryResultsMeta;
|
|
938
|
+
}
|
|
939
|
+
> {
|
|
940
|
+
let normalizedRealms = (realms ?? [])
|
|
941
|
+
.map((realm) => new RealmPaths(new URL(realm)).url)
|
|
942
|
+
.filter(Boolean);
|
|
943
|
+
let searchRealms =
|
|
944
|
+
normalizedRealms.length > 0
|
|
945
|
+
? normalizedRealms
|
|
946
|
+
: this.realmServer.availableRealmIdentifiers;
|
|
947
|
+
if (searchRealms.length === 0) {
|
|
948
|
+
if (query.asData) {
|
|
949
|
+
return opts?.includeMeta
|
|
950
|
+
? { resources: [], meta: { page: { total: 0 } } }
|
|
951
|
+
: [];
|
|
952
|
+
}
|
|
953
|
+
return opts?.includeMeta
|
|
954
|
+
? { instances: [], meta: { page: { total: 0 } } }
|
|
955
|
+
: [];
|
|
956
|
+
}
|
|
957
|
+
if (query.asData) {
|
|
958
|
+
let result = await this.fetchSearchData(query, searchRealms);
|
|
959
|
+
return opts?.includeMeta ? result : result.resources;
|
|
960
|
+
}
|
|
961
|
+
let result = await this.fetchAndHydrateSearchResults<T>(
|
|
962
|
+
query,
|
|
963
|
+
searchRealms,
|
|
964
|
+
opts?.dependencyTrackingContext,
|
|
965
|
+
);
|
|
966
|
+
return opts?.includeMeta ? result : result.instances;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
private async fetchAndHydrateSearchResults<
|
|
970
|
+
T extends CardDef | FileDef = CardDef,
|
|
971
|
+
>(
|
|
972
|
+
query: Query,
|
|
973
|
+
realms: string[],
|
|
974
|
+
dependencyTrackingContext?: RuntimeDependencyTrackingContext,
|
|
975
|
+
): Promise<{ instances: T[]; meta: QueryResultsMeta }> {
|
|
976
|
+
let collectionDoc = await this.fetchSearchDoc(query, realms);
|
|
977
|
+
|
|
978
|
+
// Hydrate each result into the store
|
|
979
|
+
let instances = (
|
|
980
|
+
await Promise.all(
|
|
981
|
+
collectionDoc.data.map(async (resource) => {
|
|
982
|
+
try {
|
|
983
|
+
return await this.addResourceFromSearchData<T>(
|
|
984
|
+
resource,
|
|
985
|
+
dependencyTrackingContext,
|
|
986
|
+
);
|
|
987
|
+
} catch (error) {
|
|
988
|
+
storeLogger.warn(
|
|
989
|
+
`Failed to hydrate resource from search results (id: ${'id' in resource ? resource.id : 'unknown'})`,
|
|
990
|
+
error,
|
|
991
|
+
);
|
|
992
|
+
return undefined;
|
|
993
|
+
}
|
|
994
|
+
}),
|
|
995
|
+
)
|
|
996
|
+
).filter(Boolean) as T[];
|
|
997
|
+
|
|
998
|
+
return { instances, meta: collectionDoc.meta };
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
private async fetchSearchData(
|
|
1002
|
+
query: Query,
|
|
1003
|
+
realms: string[],
|
|
1004
|
+
): Promise<{
|
|
1005
|
+
resources: (CardResource<Saved> | FileMetaResource)[];
|
|
1006
|
+
meta: QueryResultsMeta;
|
|
1007
|
+
}> {
|
|
1008
|
+
let doc = await this.fetchSearchDoc(query, realms);
|
|
1009
|
+
return { resources: doc.data, meta: doc.meta };
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// Shared HTTP+JSON path for both `fetchSearchData` (raw resources for
|
|
1013
|
+
// data-only callers) and `fetchAndHydrateSearchResults` (instances
|
|
1014
|
+
// hydrated into the store). Sits between `store.search` and
|
|
1015
|
+
// `_federated-search`.
|
|
1016
|
+
//
|
|
1017
|
+
// Two layers of dedup, both prerender-gated:
|
|
1018
|
+
//
|
|
1019
|
+
// 1. Resolved-doc cache (`searchCache`). Keyed by
|
|
1020
|
+
// (jobId, consumingRealm, query). Same-realm-only so a
|
|
1021
|
+
// cross-realm read can't freeze a value while a peer
|
|
1022
|
+
// realm-server replica swaps mid-job. Hit → return cached doc
|
|
1023
|
+
// synchronously, no network. Miss → fall through.
|
|
1024
|
+
// 2. In-flight Map (`inflightSearch`). Concurrent same-(realms,
|
|
1025
|
+
// query) callers share one pending fetch. Sequential repeats
|
|
1026
|
+
// that don't hit layer 1 still pay the round-trip; layer 1 is
|
|
1027
|
+
// what closes the sequential-repeat window.
|
|
1028
|
+
//
|
|
1029
|
+
// Outside a prerender both layers are bypassed so live-SPA
|
|
1030
|
+
// write-then-read flows keep their current freshness semantics.
|
|
1031
|
+
private async fetchSearchDoc(
|
|
1032
|
+
query: Query,
|
|
1033
|
+
realms: string[],
|
|
1034
|
+
): Promise<LinkableCollectionDocument> {
|
|
1035
|
+
let inPrerender = Boolean((globalThis as any).__boxelRenderContext);
|
|
1036
|
+
let jobId = inPrerender
|
|
1037
|
+
? ((globalThis as any).__boxelJobId as string | undefined)
|
|
1038
|
+
: undefined;
|
|
1039
|
+
let consumingRealm = inPrerender
|
|
1040
|
+
? ((globalThis as any).__boxelConsumingRealm as string | undefined)
|
|
1041
|
+
: undefined;
|
|
1042
|
+
|
|
1043
|
+
// Belt-and-braces jobId-change clear at fetch-entry. `resetState`
|
|
1044
|
+
// and the render-route deactivate hook are the primary paths; this
|
|
1045
|
+
// catches a prerender tab reused across jobs without either firing.
|
|
1046
|
+
if (typeof jobId === 'string' && jobId !== this.searchCacheJobId) {
|
|
1047
|
+
this.searchCache.clear();
|
|
1048
|
+
this.searchCacheJobId = jobId;
|
|
1049
|
+
this.searchCacheGeneration++;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// Resolved-doc cache eligibility: prerender + jobId + same-realm.
|
|
1053
|
+
// Cross-realm reads bypass — see field comment.
|
|
1054
|
+
let cacheKey: string | undefined;
|
|
1055
|
+
if (
|
|
1056
|
+
inPrerender &&
|
|
1057
|
+
typeof jobId === 'string' &&
|
|
1058
|
+
typeof consumingRealm === 'string' &&
|
|
1059
|
+
realms.length === 1 &&
|
|
1060
|
+
realms[0] === consumingRealm
|
|
1061
|
+
) {
|
|
1062
|
+
cacheKey = searchCacheKey(jobId, consumingRealm, query);
|
|
1063
|
+
if (cacheKey !== undefined) {
|
|
1064
|
+
let cached = this.searchCache.get(cacheKey);
|
|
1065
|
+
if (cached !== undefined) {
|
|
1066
|
+
return cached;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
// Snapshot the generation *after* the entry-time clear so a
|
|
1071
|
+
// concurrent clear arriving during the await below is observable
|
|
1072
|
+
// as a generation drift and we skip the populate. Mirrors the
|
|
1073
|
+
// identity check used by the in-flight Map below.
|
|
1074
|
+
let captureGeneration = this.searchCacheGeneration;
|
|
1075
|
+
|
|
1076
|
+
let inflightKey = inPrerender
|
|
1077
|
+
? searchInFlightKey(realms, query)
|
|
1078
|
+
: undefined;
|
|
1079
|
+
let doc: LinkableCollectionDocument;
|
|
1080
|
+
if (inflightKey !== undefined) {
|
|
1081
|
+
let existing = this.inflightSearch.get(inflightKey);
|
|
1082
|
+
if (existing) {
|
|
1083
|
+
doc = await existing;
|
|
1084
|
+
} else {
|
|
1085
|
+
let pending = this.fetchSearchDocUncoalesced(query, realms).finally(
|
|
1086
|
+
() => {
|
|
1087
|
+
// Identity-check before deletion: a concurrent
|
|
1088
|
+
// `clearInFlightSearch()` could in principle have removed
|
|
1089
|
+
// (and a later caller re-set) this slot while we were
|
|
1090
|
+
// in-flight. Only clean up if the map still points at *this*
|
|
1091
|
+
// pending promise. Mirrors
|
|
1092
|
+
// `RealmIndexQueryEngine.searchCards` server-side.
|
|
1093
|
+
if (this.inflightSearch.get(inflightKey) === pending) {
|
|
1094
|
+
this.inflightSearch.delete(inflightKey);
|
|
1095
|
+
}
|
|
1096
|
+
},
|
|
1097
|
+
);
|
|
1098
|
+
this.inflightSearch.set(inflightKey, pending);
|
|
1099
|
+
doc = await pending;
|
|
1100
|
+
}
|
|
1101
|
+
} else {
|
|
1102
|
+
doc = await this.fetchSearchDocUncoalesced(query, realms);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// Populate only if the cache generation hasn't moved under us. A
|
|
1106
|
+
// route deactivate (clearSearchCache) or `resetState` between
|
|
1107
|
+
// fetch-entry and resolve would bump the generation; in that case
|
|
1108
|
+
// the resolved doc belongs to a now-stale window and must not
|
|
1109
|
+
// repopulate the cleared cache. The caller still receives `doc`
|
|
1110
|
+
// — only the *cache write* is suppressed.
|
|
1111
|
+
if (
|
|
1112
|
+
cacheKey !== undefined &&
|
|
1113
|
+
this.searchCacheGeneration === captureGeneration
|
|
1114
|
+
) {
|
|
1115
|
+
this.searchCache.set(cacheKey, doc);
|
|
1116
|
+
}
|
|
1117
|
+
return doc;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
private async fetchSearchDocUncoalesced(
|
|
1121
|
+
query: Query,
|
|
1122
|
+
realms: string[],
|
|
1123
|
+
): Promise<LinkableCollectionDocument> {
|
|
1124
|
+
let realmServerURLs = this.realmServer.getRealmServersForRealms(realms);
|
|
1125
|
+
// TODO remove this assertion after multi-realm server/federated identity is supported
|
|
1126
|
+
this.realmServer.assertOwnRealmServer(realmServerURLs);
|
|
1127
|
+
let [realmServerURL] = realmServerURLs;
|
|
1128
|
+
let searchURL = new URL('_federated-search', realmServerURL);
|
|
1129
|
+
let response = await this.realmServer.maybeAuthedFetchForRealms(
|
|
1130
|
+
searchURL.href,
|
|
1131
|
+
realms,
|
|
1132
|
+
{
|
|
1133
|
+
method: 'QUERY',
|
|
1134
|
+
headers: {
|
|
1135
|
+
Accept: SupportedMimeType.CardJson,
|
|
1136
|
+
'Content-Type': 'application/json',
|
|
1137
|
+
...duringPrerenderHeaders(),
|
|
1138
|
+
...consumingRealmHeader(),
|
|
1139
|
+
...jobIdHeader(),
|
|
1140
|
+
...jobPriorityHeader(),
|
|
1141
|
+
},
|
|
1142
|
+
body: JSON.stringify({ ...query, realms }),
|
|
1143
|
+
},
|
|
1144
|
+
);
|
|
1145
|
+
if (!response.ok) {
|
|
1146
|
+
let responseText = await response.text();
|
|
1147
|
+
let err = new Error(
|
|
1148
|
+
`status: ${response.status} - ${response.statusText}. ${responseText}`,
|
|
1149
|
+
) as any;
|
|
1150
|
+
err.status = response.status;
|
|
1151
|
+
err.responseText = responseText;
|
|
1152
|
+
err.responseHeaders = response.headers;
|
|
1153
|
+
throw err;
|
|
1154
|
+
}
|
|
1155
|
+
let json = await response.json();
|
|
1156
|
+
if (!isLinkableCollectionDocument(json)) {
|
|
1157
|
+
throw new Error(
|
|
1158
|
+
`The realm search response was not a valid collection document:
|
|
1159
|
+
${JSON.stringify(json, null, 2)}`,
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
return json;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
getSearchResource<T extends CardDef | FileDef = CardDef>(
|
|
1166
|
+
parent: object,
|
|
1167
|
+
getQuery: () => Query | undefined,
|
|
1168
|
+
getRealms?: () => string[] | undefined,
|
|
1169
|
+
opts?: {
|
|
1170
|
+
isLive?: boolean;
|
|
1171
|
+
doWhileRefreshing?: (() => void) | undefined;
|
|
1172
|
+
dependencyTracking?: RuntimeDependencyTrackingContext;
|
|
1173
|
+
seed?: {
|
|
1174
|
+
cards: T[];
|
|
1175
|
+
searchURL?: string;
|
|
1176
|
+
meta?: QueryResultsMeta;
|
|
1177
|
+
errors?: ErrorEntry[];
|
|
1178
|
+
queryErrors?: Array<{
|
|
1179
|
+
realm: string;
|
|
1180
|
+
type: string;
|
|
1181
|
+
message: string;
|
|
1182
|
+
status?: number;
|
|
1183
|
+
}>;
|
|
1184
|
+
};
|
|
1185
|
+
},
|
|
1186
|
+
): SearchResource<T> {
|
|
1187
|
+
if (this.isRenderStore && opts) {
|
|
1188
|
+
opts.isLive = false;
|
|
1189
|
+
}
|
|
1190
|
+
return getSearch<T>(parent, getOwner(this)!, getQuery, getRealms, {
|
|
1191
|
+
...opts,
|
|
1192
|
+
storeService: this,
|
|
1193
|
+
}) as unknown as SearchResource<T>;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
getSearchDataResource(
|
|
1197
|
+
parent: object,
|
|
1198
|
+
getQuery: () => DataQuery | undefined,
|
|
1199
|
+
getRealms?: () => string[] | undefined,
|
|
1200
|
+
opts?: { isLive?: boolean },
|
|
1201
|
+
): SearchDataResource {
|
|
1202
|
+
if (this.isRenderStore && opts) {
|
|
1203
|
+
opts.isLive = false;
|
|
1204
|
+
}
|
|
1205
|
+
return getSearchData(
|
|
1206
|
+
parent,
|
|
1207
|
+
getOwner(this)!,
|
|
1208
|
+
getQuery,
|
|
1209
|
+
getRealms,
|
|
1210
|
+
opts,
|
|
1211
|
+
) as unknown as SearchDataResource;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
getSaveState(id: string): AutoSaveState | undefined {
|
|
1215
|
+
id = asURL(id);
|
|
1216
|
+
return this.autoSaveStates.get(id);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
async flush() {
|
|
1220
|
+
await this.ready;
|
|
1221
|
+
await Promise.allSettled(this.newReferencePromises);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
async flushSaves() {
|
|
1225
|
+
await Promise.allSettled(this.autoSavePromises.values());
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
getReferenceCount(id: string) {
|
|
1229
|
+
id = asURL(id);
|
|
1230
|
+
return this.referenceCount.get(id) ?? 0;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
isSameId(a: string, b: string): boolean {
|
|
1234
|
+
return a === b || this.peek(a) === this.peek(b);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
async waitForCardLoad(cardId: string): Promise<void> {
|
|
1238
|
+
let normalizedId = asURL(cardId);
|
|
1239
|
+
if (!normalizedId) {
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
let inflightLoad = this.inflightCardLoads.get(normalizedId);
|
|
1243
|
+
if (inflightLoad) {
|
|
1244
|
+
await inflightLoad.promise;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
private startTrackingCardLoad(
|
|
1249
|
+
cardId: string | undefined,
|
|
1250
|
+
): Deferred<void> | undefined {
|
|
1251
|
+
if (!cardId) {
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
let normalizedId = asURL(cardId);
|
|
1255
|
+
if (!normalizedId) {
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
let deferred = new Deferred<void>();
|
|
1259
|
+
this.inflightCardLoads.set(normalizedId, deferred);
|
|
1260
|
+
return deferred;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
private finishTrackingCardLoad(
|
|
1264
|
+
cardId: string | undefined,
|
|
1265
|
+
deferred?: Deferred<void>,
|
|
1266
|
+
) {
|
|
1267
|
+
if (!cardId || !deferred) {
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
let normalizedId = asURL(cardId);
|
|
1271
|
+
if (!normalizedId) {
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
let current = this.inflightCardLoads.get(normalizedId);
|
|
1275
|
+
if (current === deferred) {
|
|
1276
|
+
this.inflightCardLoads.delete(normalizedId);
|
|
1277
|
+
}
|
|
1278
|
+
deferred.fulfill();
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
private async wireUpNewReference(
|
|
1282
|
+
url: string,
|
|
1283
|
+
readType: StoreReadType = 'card',
|
|
1284
|
+
) {
|
|
1285
|
+
let deferred = new Deferred<void>();
|
|
1286
|
+
await this.withTestWaiters(async () => {
|
|
1287
|
+
this.newReferencePromises.push(deferred.promise);
|
|
1288
|
+
try {
|
|
1289
|
+
await this.ready;
|
|
1290
|
+
if (readType === 'file-meta') {
|
|
1291
|
+
let instanceOrError = await this.getFileMetaInstance<FileDef>({
|
|
1292
|
+
idOrDoc: url,
|
|
1293
|
+
});
|
|
1294
|
+
this.setIdentityContext(
|
|
1295
|
+
instanceOrError as FileDef | CardErrorJSONAPI,
|
|
1296
|
+
'file-meta',
|
|
1297
|
+
);
|
|
1298
|
+
deferred.fulfill();
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
// Check file-meta map as well as card map — file-meta instances
|
|
1302
|
+
// are loaded into their own map by store.get(id, { type: 'file-meta' })
|
|
1303
|
+
let fileMetaInstance =
|
|
1304
|
+
this.peekError(url, { type: 'file-meta' }) ??
|
|
1305
|
+
this.peek(url, { type: 'file-meta' });
|
|
1306
|
+
if (fileMetaInstance) {
|
|
1307
|
+
// File-meta instances don't need auto-saving or card wiring
|
|
1308
|
+
deferred.fulfill();
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
let instanceOrError = this.peekError(url) ?? this.peek(url);
|
|
1312
|
+
if (!instanceOrError) {
|
|
1313
|
+
instanceOrError = await this.getCardInstance({
|
|
1314
|
+
idOrDoc: url,
|
|
1315
|
+
});
|
|
1316
|
+
this.setIdentityContext(instanceOrError);
|
|
1317
|
+
}
|
|
1318
|
+
await this.startAutoSaving(instanceOrError);
|
|
1319
|
+
if (!instanceOrError.id) {
|
|
1320
|
+
// keep track of urls for cards that are missing
|
|
1321
|
+
this.store.addCardInstanceOrError(url, instanceOrError);
|
|
1322
|
+
}
|
|
1323
|
+
deferred.fulfill();
|
|
1324
|
+
} catch (e) {
|
|
1325
|
+
console.error(
|
|
1326
|
+
`error encountered wiring up new reference for ${JSON.stringify(url)}`,
|
|
1327
|
+
e,
|
|
1328
|
+
);
|
|
1329
|
+
deferred.reject(e);
|
|
1330
|
+
}
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
/**
|
|
1335
|
+
* Low-level deserialization that throws on validation errors.
|
|
1336
|
+
*
|
|
1337
|
+
* Most callers should use `add()` or `create()` instead — those methods
|
|
1338
|
+
* handle persistence, identity mapping, and auto-saving. This method
|
|
1339
|
+
* bypasses all of that and calls `card-api.createFromSerialized` directly.
|
|
1340
|
+
*
|
|
1341
|
+
* `store.add()` relaxes serialization errors: `Field.validate()` failures
|
|
1342
|
+
* during deserialization are caught internally and logged as console warnings
|
|
1343
|
+
* rather than thrown. This is correct for the UI but not for validation use
|
|
1344
|
+
* cases where errors must propagate. Use this method only when you need
|
|
1345
|
+
* validation errors to throw (e.g., the software-factory's instantiate-card
|
|
1346
|
+
* command which validates that a card instance can be deserialized).
|
|
1347
|
+
*/
|
|
1348
|
+
async __dangerousCreateFromSerialized<T extends CardDef>(
|
|
1349
|
+
resource: LooseCardResource,
|
|
1350
|
+
doc: LooseSingleCardDocument | CardDocument,
|
|
1351
|
+
relativeTo?: URL | undefined,
|
|
1352
|
+
dependencyTrackingContext?: RuntimeDependencyTrackingContext,
|
|
1353
|
+
): Promise<T> {
|
|
1354
|
+
return this.createFromSerialized(
|
|
1355
|
+
resource,
|
|
1356
|
+
doc,
|
|
1357
|
+
relativeTo,
|
|
1358
|
+
dependencyTrackingContext,
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
private async createFromSerialized<T extends CardDef>(
|
|
1363
|
+
resource: LooseCardResource,
|
|
1364
|
+
doc: LooseSingleCardDocument | CardDocument,
|
|
1365
|
+
relativeTo?: URL | undefined,
|
|
1366
|
+
dependencyTrackingContext?: RuntimeDependencyTrackingContext,
|
|
1367
|
+
): Promise<T> {
|
|
1368
|
+
let api = await this.cardService.getAPI();
|
|
1369
|
+
let shouldStubTimers =
|
|
1370
|
+
this.renderContextBlocksPersistence() && !isTesting();
|
|
1371
|
+
let performCreate = async () =>
|
|
1372
|
+
(await api.createFromSerialized(resource, doc, relativeTo, {
|
|
1373
|
+
store: this.store,
|
|
1374
|
+
dependencyTrackingContext,
|
|
1375
|
+
})) as T;
|
|
1376
|
+
let card = shouldStubTimers
|
|
1377
|
+
? await withStubbedRenderTimers(performCreate)
|
|
1378
|
+
: await performCreate();
|
|
1379
|
+
return card;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
private async setup() {
|
|
1383
|
+
let api = await this.cardService.getAPI();
|
|
1384
|
+
if (isDestroyed(this) || isDestroying(this)) {
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
this.gcInterval = setInterval(
|
|
1388
|
+
() => this.store.sweep(api),
|
|
1389
|
+
2 * 60_000,
|
|
1390
|
+
) as unknown as number;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
private unsubscribeFromInstance(id: string) {
|
|
1394
|
+
let instance = this.store.getCard(id);
|
|
1395
|
+
if (instance && this.cardApiCache) {
|
|
1396
|
+
this.cardApiCache.unsubscribeFromChanges(
|
|
1397
|
+
instance,
|
|
1398
|
+
this.onInstanceUpdated,
|
|
1399
|
+
);
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
// if there are no more subscribers to this realm then unsubscribe from realm
|
|
1403
|
+
let realmHref = !isLocalId(id)
|
|
1404
|
+
? [...this.subscriptions.keys()].find((realmURL) =>
|
|
1405
|
+
id.startsWith(realmURL),
|
|
1406
|
+
)
|
|
1407
|
+
: undefined;
|
|
1408
|
+
if (!realmHref) {
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
let subscription = this.subscriptions.get(realmHref);
|
|
1413
|
+
if (
|
|
1414
|
+
subscription &&
|
|
1415
|
+
![...this.referenceCount.entries()].find(
|
|
1416
|
+
([referenceId, count]) =>
|
|
1417
|
+
!isLocalId(referenceId) &&
|
|
1418
|
+
count > 0 &&
|
|
1419
|
+
referenceId.startsWith(realmHref),
|
|
1420
|
+
)
|
|
1421
|
+
) {
|
|
1422
|
+
subscription.unsubscribe();
|
|
1423
|
+
this.subscriptions.delete(realmHref);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
private createCardStore(): CardStore {
|
|
1428
|
+
return new CardStore(this.referenceCount, this.network.authedFetch, {
|
|
1429
|
+
getSearchResource: (parent, getQuery, getRealms, opts) =>
|
|
1430
|
+
this.getSearchResource(parent, getQuery, getRealms, opts),
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
private handleInvalidations = (event: RealmEventContent) => {
|
|
1435
|
+
if (event.eventName !== 'index') {
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
if (event.indexType !== 'incremental') {
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
let invalidations = event.invalidations as string[];
|
|
1443
|
+
|
|
1444
|
+
if (
|
|
1445
|
+
invalidations.find(
|
|
1446
|
+
(i) =>
|
|
1447
|
+
hasExecutableExtension(i) &&
|
|
1448
|
+
this.loaderService.loader.isModuleLoaded(i),
|
|
1449
|
+
)
|
|
1450
|
+
) {
|
|
1451
|
+
// the invalidation included code changes to modules that are already
|
|
1452
|
+
// loaded. in this case we need to flush the loader so that we can pick
|
|
1453
|
+
// up the updated code before re-running the card. net-new modules that
|
|
1454
|
+
// have never been loaded don't require a loader reset.
|
|
1455
|
+
this.loaderService.resetLoader();
|
|
1456
|
+
this.store.reset();
|
|
1457
|
+
this.reestablishReferences.perform();
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
for (let invalidation of invalidations) {
|
|
1461
|
+
if (hasExecutableExtension(invalidation)) {
|
|
1462
|
+
// we already dealt with this
|
|
1463
|
+
continue;
|
|
1464
|
+
}
|
|
1465
|
+
let fileMetaInstance =
|
|
1466
|
+
this.peekError(invalidation, { type: 'file-meta' }) ??
|
|
1467
|
+
this.peek(invalidation, { type: 'file-meta' });
|
|
1468
|
+
if (fileMetaInstance) {
|
|
1469
|
+
realmEventsLogger.debug(
|
|
1470
|
+
`reloading file-meta resource ${invalidation} because it was previously loaded`,
|
|
1471
|
+
);
|
|
1472
|
+
this.reloadFileMetaTask.perform(invalidation);
|
|
1473
|
+
}
|
|
1474
|
+
let clientRequestId = event.clientRequestId ?? undefined;
|
|
1475
|
+
|
|
1476
|
+
let instance = this.peekError(invalidation) ?? this.peek(invalidation);
|
|
1477
|
+
if (instance) {
|
|
1478
|
+
if (isCardInstance(instance)) {
|
|
1479
|
+
// Do not reload if the event is a result of an instance-editing request that we made. Otherwise we risk
|
|
1480
|
+
// overwriting the inputs with past values. This can happen if the user makes edits in the time between
|
|
1481
|
+
// the auto save request and the arrival realm event.
|
|
1482
|
+
let reloadFile = false;
|
|
1483
|
+
|
|
1484
|
+
if (!clientRequestId) {
|
|
1485
|
+
reloadFile = true;
|
|
1486
|
+
realmEventsLogger.debug(
|
|
1487
|
+
`reloading file resource ${invalidation} because event has no clientRequestId`,
|
|
1488
|
+
);
|
|
1489
|
+
} else if (this.cardService.clientRequestIds.has(clientRequestId)) {
|
|
1490
|
+
if (
|
|
1491
|
+
clientRequestId.startsWith('instance:') ||
|
|
1492
|
+
clientRequestId.startsWith('editor-with-instance')
|
|
1493
|
+
) {
|
|
1494
|
+
realmEventsLogger.debug(
|
|
1495
|
+
`ignoring invalidation for card ${invalidation} because request id ${clientRequestId} is ours and an instance type`,
|
|
1496
|
+
);
|
|
1497
|
+
} else {
|
|
1498
|
+
reloadFile = true;
|
|
1499
|
+
realmEventsLogger.debug(
|
|
1500
|
+
`reloading file resource ${invalidation} because request id ${clientRequestId} is not instance type`,
|
|
1501
|
+
);
|
|
1502
|
+
}
|
|
1503
|
+
} else {
|
|
1504
|
+
reloadFile = true;
|
|
1505
|
+
realmEventsLogger.debug(
|
|
1506
|
+
`reloading file resource ${invalidation} because request id ${clientRequestId} is not contained within known clientRequestIds`,
|
|
1507
|
+
Array.from(this.cardService.clientRequestIds.values()),
|
|
1508
|
+
);
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
if (reloadFile) {
|
|
1512
|
+
this.reloadTask.perform(instance);
|
|
1513
|
+
} else {
|
|
1514
|
+
realmEventsLogger.debug(
|
|
1515
|
+
`ignoring invalidation ${invalidation} for request id ${clientRequestId}`,
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
} else {
|
|
1519
|
+
realmEventsLogger.debug(
|
|
1520
|
+
`reloading file resource ${invalidation} because it is in an error state`,
|
|
1521
|
+
);
|
|
1522
|
+
this.loadInstanceTask.perform(invalidation);
|
|
1523
|
+
}
|
|
1524
|
+
} else {
|
|
1525
|
+
realmEventsLogger.debug(
|
|
1526
|
+
`ignoring invalidation ${invalidation} because we did not previously try to load it`,
|
|
1527
|
+
);
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
};
|
|
1531
|
+
|
|
1532
|
+
private loadInstanceTask = task(
|
|
1533
|
+
async (idOrDoc: string | LooseSingleCardDocument) => {
|
|
1534
|
+
let url = asURL(idOrDoc);
|
|
1535
|
+
let reloadTracker = this.startTrackingCardLoad(url);
|
|
1536
|
+
try {
|
|
1537
|
+
let oldInstance = url ? this.store.getCard(url) : undefined;
|
|
1538
|
+
let instanceOrError = await this.getCardInstance({
|
|
1539
|
+
idOrDoc,
|
|
1540
|
+
opts: { noCache: true },
|
|
1541
|
+
});
|
|
1542
|
+
if (oldInstance) {
|
|
1543
|
+
await this.stopAutoSaving(oldInstance);
|
|
1544
|
+
}
|
|
1545
|
+
this.setIdentityContext(instanceOrError);
|
|
1546
|
+
await this.startAutoSaving(instanceOrError);
|
|
1547
|
+
} finally {
|
|
1548
|
+
this.finishTrackingCardLoad(url, reloadTracker);
|
|
1549
|
+
}
|
|
1550
|
+
},
|
|
1551
|
+
);
|
|
1552
|
+
|
|
1553
|
+
private reestablishReferences = task(async () => {
|
|
1554
|
+
let remoteIds = new Set<string>();
|
|
1555
|
+
for (let [id, referenceCount] of this.referenceCount) {
|
|
1556
|
+
if (referenceCount === 0) {
|
|
1557
|
+
continue;
|
|
1558
|
+
}
|
|
1559
|
+
if (isLocalId(id)) {
|
|
1560
|
+
let remoteIdsForLocal = this.store.getRemoteIds(id);
|
|
1561
|
+
if (remoteIdsForLocal.length === 0) {
|
|
1562
|
+
let error = this.store.getCardError(id);
|
|
1563
|
+
if (error?.meta?.remoteId) {
|
|
1564
|
+
remoteIdsForLocal = [error.meta.remoteId];
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
for (let remoteId of remoteIdsForLocal) {
|
|
1568
|
+
remoteIds.add(remoteId);
|
|
1569
|
+
}
|
|
1570
|
+
} else {
|
|
1571
|
+
remoteIds.add(id);
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
await Promise.all(
|
|
1575
|
+
[...remoteIds].map((id) => this.getCardInstance({ idOrDoc: id })),
|
|
1576
|
+
);
|
|
1577
|
+
});
|
|
1578
|
+
|
|
1579
|
+
private reloadTask = task(async (instance: CardDef) => {
|
|
1580
|
+
let reloadTracker = this.startTrackingCardLoad(instance.id);
|
|
1581
|
+
let maybeReloadedInstance: CardDef | CardErrorJSONAPI | undefined;
|
|
1582
|
+
let isDelete = false;
|
|
1583
|
+
|
|
1584
|
+
try {
|
|
1585
|
+
try {
|
|
1586
|
+
await this.reloadInstance(instance);
|
|
1587
|
+
maybeReloadedInstance = instance;
|
|
1588
|
+
} catch (err: any) {
|
|
1589
|
+
if (err.status === 404) {
|
|
1590
|
+
// in this case the document was invalidated in the index because the
|
|
1591
|
+
// file was deleted
|
|
1592
|
+
isDelete = true;
|
|
1593
|
+
} else {
|
|
1594
|
+
let errorResponse = processCardError(instance.id, err);
|
|
1595
|
+
maybeReloadedInstance = errorResponse.errors[0];
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
if (!isCardInstance(maybeReloadedInstance)) {
|
|
1599
|
+
await this.stopAutoSaving(instance);
|
|
1600
|
+
}
|
|
1601
|
+
if (maybeReloadedInstance) {
|
|
1602
|
+
this.setIdentityContext(maybeReloadedInstance);
|
|
1603
|
+
await this.startAutoSaving(maybeReloadedInstance);
|
|
1604
|
+
}
|
|
1605
|
+
if (isDelete) {
|
|
1606
|
+
await this.stopAutoSaving(instance);
|
|
1607
|
+
this.store.delete(instance.id);
|
|
1608
|
+
}
|
|
1609
|
+
} finally {
|
|
1610
|
+
this.finishTrackingCardLoad(instance.id, reloadTracker);
|
|
1611
|
+
}
|
|
1612
|
+
});
|
|
1613
|
+
|
|
1614
|
+
private reloadFileMetaTask = task(async (url: string) => {
|
|
1615
|
+
await this.withTestWaiters(async () => {
|
|
1616
|
+
let instanceOrError = await this.getFileMetaInstance<FileDef>({
|
|
1617
|
+
idOrDoc: url,
|
|
1618
|
+
opts: { noCache: true },
|
|
1619
|
+
});
|
|
1620
|
+
this.setIdentityContext(
|
|
1621
|
+
instanceOrError as FileDef | CardErrorJSONAPI,
|
|
1622
|
+
'file-meta',
|
|
1623
|
+
);
|
|
1624
|
+
});
|
|
1625
|
+
});
|
|
1626
|
+
|
|
1627
|
+
private onInstanceUpdated = (instance: BaseDef, fieldName: string) => {
|
|
1628
|
+
if (fieldName === 'id') {
|
|
1629
|
+
// id updates are internal and do not trigger autosaves
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
if (isCardInstance(instance)) {
|
|
1633
|
+
let autoSaveState = this.initOrGetAutoSaveState(instance);
|
|
1634
|
+
autoSaveState.hasUnsavedChanges = true;
|
|
1635
|
+
this.doAutoSave(instance);
|
|
1636
|
+
}
|
|
1637
|
+
};
|
|
1638
|
+
|
|
1639
|
+
private setIdentityContext(
|
|
1640
|
+
instanceOrError: CardDef | FileDef | CardErrorJSONAPI,
|
|
1641
|
+
readType: StoreReadType = 'card',
|
|
1642
|
+
) {
|
|
1643
|
+
if (readType === 'file-meta') {
|
|
1644
|
+
let id = (instanceOrError as { id?: string }).id;
|
|
1645
|
+
if (!id) {
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
this.store.addFileMetaInstanceOrError(
|
|
1649
|
+
id,
|
|
1650
|
+
instanceOrError as FileDef | CardErrorJSONAPI,
|
|
1651
|
+
);
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
let instance = isCardInstance(instanceOrError)
|
|
1656
|
+
? instanceOrError
|
|
1657
|
+
: undefined;
|
|
1658
|
+
if (!instance && !instanceOrError.id) {
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
this.store.addCardInstanceOrError(
|
|
1662
|
+
instance ? (instance.id ?? instance[localIdSymbol]) : instanceOrError.id!, // we checked above to make sure errors have id's
|
|
1663
|
+
instanceOrError as CardDef | CardErrorJSONAPI,
|
|
1664
|
+
);
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
protected async createFileMetaFromSerialized(
|
|
1668
|
+
resource: LooseLinkableResource<FileMetaResource>,
|
|
1669
|
+
doc: LooseSingleResourceDocument<FileMetaResource>,
|
|
1670
|
+
relativeTo: URL | undefined,
|
|
1671
|
+
dependencyTrackingContext?: RuntimeDependencyTrackingContext,
|
|
1672
|
+
): Promise<FileDef> {
|
|
1673
|
+
let api = await this.cardService.getAPI();
|
|
1674
|
+
let instance = (await api.createFromSerialized(resource, doc, relativeTo, {
|
|
1675
|
+
store: this.store,
|
|
1676
|
+
dependencyTrackingContext,
|
|
1677
|
+
})) as unknown as FileDef;
|
|
1678
|
+
this.setIdentityContext(instance, 'file-meta');
|
|
1679
|
+
return instance;
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
// Internal method for hydrating a resource from search response data.
|
|
1683
|
+
// This avoids N+1 queries when search results include card or file-meta resources.
|
|
1684
|
+
// Not part of the public API since it's meant for internal search result processing.
|
|
1685
|
+
private async addResourceFromSearchData<T extends CardDef | FileDef>(
|
|
1686
|
+
resource: CardResource<Saved> | FileMetaResource,
|
|
1687
|
+
dependencyTrackingContext?: RuntimeDependencyTrackingContext,
|
|
1688
|
+
): Promise<T | undefined> {
|
|
1689
|
+
if (!resource.id) {
|
|
1690
|
+
throw new Error('resource must have an id');
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
// Handle file-meta resources
|
|
1694
|
+
if (isFileMetaResource(resource)) {
|
|
1695
|
+
let existingInstance = this.peek(resource.id, { type: 'file-meta' });
|
|
1696
|
+
if (existingInstance && isFileDefInstance(existingInstance)) {
|
|
1697
|
+
return existingInstance as T;
|
|
1698
|
+
}
|
|
1699
|
+
let doc = { data: resource };
|
|
1700
|
+
return this.createFileMetaFromSerialized(
|
|
1701
|
+
resource,
|
|
1702
|
+
doc,
|
|
1703
|
+
cardIdToURL(resource.id),
|
|
1704
|
+
dependencyTrackingContext,
|
|
1705
|
+
) as Promise<T>;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
// Handle card resources
|
|
1709
|
+
let existingInstance = this.peek(resource.id);
|
|
1710
|
+
if (existingInstance && isCardInstance(existingInstance)) {
|
|
1711
|
+
return existingInstance as T;
|
|
1712
|
+
}
|
|
1713
|
+
// Mark resources that came from `_search` so query-field seed handling can
|
|
1714
|
+
// distinguish unresolved empty seeds from explicit empty card-GET results.
|
|
1715
|
+
(resource as any)[queryFieldSeedFromSearchSymbol] = true;
|
|
1716
|
+
return this.add({ data: resource } as SingleCardDocument, {
|
|
1717
|
+
doNotPersist: true,
|
|
1718
|
+
relativeTo: cardIdToURL(resource.id),
|
|
1719
|
+
dependencyTrackingContext,
|
|
1720
|
+
}) as Promise<T>;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
private async startAutoSaving(instanceOrError: CardDef | CardErrorJSONAPI) {
|
|
1724
|
+
if (!isCardInstance(instanceOrError)) {
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
let instance = instanceOrError;
|
|
1728
|
+
// module updates will break the cached api. so don't hang on to this longer
|
|
1729
|
+
// than necessary
|
|
1730
|
+
this.cardApiCache = await this.cardService.getAPI();
|
|
1731
|
+
this.cardApiCache.unsubscribeFromChanges(instance, this.onInstanceUpdated);
|
|
1732
|
+
this.cardApiCache.subscribeToChanges(instance, this.onInstanceUpdated);
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
private async stopAutoSaving(instanceOrError: CardDef | CardErrorJSONAPI) {
|
|
1736
|
+
if (!isCardInstance(instanceOrError)) {
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
let instance = instanceOrError;
|
|
1740
|
+
// module updates will break the cached api. so don't hang on to this longer
|
|
1741
|
+
// than necessary
|
|
1742
|
+
this.cardApiCache = await this.cardService.getAPI();
|
|
1743
|
+
this.cardApiCache.unsubscribeFromChanges(instance, this.onInstanceUpdated);
|
|
1744
|
+
this.autoSaveStates.delete(instance.id);
|
|
1745
|
+
this.autoSaveStates.delete(instance[localIdSymbol]);
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
private async getCardInstance<T extends CardDef>({
|
|
1749
|
+
idOrDoc,
|
|
1750
|
+
relativeTo,
|
|
1751
|
+
realm,
|
|
1752
|
+
opts,
|
|
1753
|
+
}: {
|
|
1754
|
+
idOrDoc: string | LooseSingleCardDocument;
|
|
1755
|
+
relativeTo?: URL;
|
|
1756
|
+
realm?: string; // used for new cards
|
|
1757
|
+
opts?: {
|
|
1758
|
+
noCache?: boolean;
|
|
1759
|
+
localDir?: string;
|
|
1760
|
+
dependencyTrackingContext?: RuntimeDependencyTrackingContext;
|
|
1761
|
+
};
|
|
1762
|
+
}): Promise<T | CardErrorJSONAPI> {
|
|
1763
|
+
let deferred: Deferred<T | CardErrorJSONAPI> | undefined;
|
|
1764
|
+
let id = asURL(idOrDoc);
|
|
1765
|
+
if (id) {
|
|
1766
|
+
let working = this.inflightGetCards.get(id);
|
|
1767
|
+
if (working) {
|
|
1768
|
+
return working as Promise<T | CardErrorJSONAPI>;
|
|
1769
|
+
}
|
|
1770
|
+
deferred = new Deferred<T | CardErrorJSONAPI>();
|
|
1771
|
+
this.inflightGetCards.set(
|
|
1772
|
+
id,
|
|
1773
|
+
deferred.promise as Promise<CardDef | CardErrorJSONAPI>,
|
|
1774
|
+
);
|
|
1775
|
+
}
|
|
1776
|
+
try {
|
|
1777
|
+
if (!id) {
|
|
1778
|
+
if (!this.renderContextBlocksPersistence()) {
|
|
1779
|
+
// this is a new card so instantiate it and save it
|
|
1780
|
+
let doc = idOrDoc as LooseSingleCardDocument;
|
|
1781
|
+
let newInstance = await this.createFromSerialized(
|
|
1782
|
+
doc.data,
|
|
1783
|
+
doc,
|
|
1784
|
+
relativeTo,
|
|
1785
|
+
opts?.dependencyTrackingContext,
|
|
1786
|
+
);
|
|
1787
|
+
let maybeError = await this.persistAndUpdate(newInstance, {
|
|
1788
|
+
realm,
|
|
1789
|
+
localDir: opts?.localDir,
|
|
1790
|
+
});
|
|
1791
|
+
if (!isCardInstance(maybeError)) {
|
|
1792
|
+
return maybeError;
|
|
1793
|
+
}
|
|
1794
|
+
this.store.setCard(newInstance.id, newInstance);
|
|
1795
|
+
deferred?.fulfill(newInstance as T);
|
|
1796
|
+
return newInstance as T;
|
|
1797
|
+
} else {
|
|
1798
|
+
throw new Error(`cannot save serialized doc in render context`);
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
let existingInstance = this.peek(id);
|
|
1803
|
+
if (!opts?.noCache && existingInstance) {
|
|
1804
|
+
deferred?.fulfill(existingInstance as T | CardErrorJSONAPI);
|
|
1805
|
+
return existingInstance as T;
|
|
1806
|
+
}
|
|
1807
|
+
if (isLocalId(id) && !isRegisteredPrefix(id)) {
|
|
1808
|
+
// we might have lost the local id via a loader refresh, try loading from remote id instead
|
|
1809
|
+
let remoteId = this.store.getRemoteIds(id)?.[0];
|
|
1810
|
+
if (!remoteId) {
|
|
1811
|
+
throw new Error(
|
|
1812
|
+
`instance with local id ${id} does not exist in the store`,
|
|
1813
|
+
);
|
|
1814
|
+
}
|
|
1815
|
+
id = remoteId;
|
|
1816
|
+
}
|
|
1817
|
+
// Resolve registered prefix IDs (e.g. @cardstack/skills/...) to actual
|
|
1818
|
+
// URLs so they can be used for fetching.
|
|
1819
|
+
let url = isRegisteredPrefix(id) ? cardIdToURL(id).href : id;
|
|
1820
|
+
let doc = (typeof idOrDoc !== 'string' ? idOrDoc : undefined) as
|
|
1821
|
+
| SingleCardDocument
|
|
1822
|
+
| undefined;
|
|
1823
|
+
if (!doc) {
|
|
1824
|
+
let json: CardDocument | undefined;
|
|
1825
|
+
if (this.isRenderStore && (globalThis as any).__boxelRenderContext) {
|
|
1826
|
+
let result = await this.cardService.getSource(
|
|
1827
|
+
cardIdToURL(`${url}.json`),
|
|
1828
|
+
);
|
|
1829
|
+
if (result.status === 200) {
|
|
1830
|
+
json = JSON.parse(result.content);
|
|
1831
|
+
} else {
|
|
1832
|
+
throw new Error(
|
|
1833
|
+
`Received non-200 status fetching instance source ${url}.json: ${result.content}`,
|
|
1834
|
+
);
|
|
1835
|
+
}
|
|
1836
|
+
} else {
|
|
1837
|
+
json = await this.cardService.fetchJSON(url);
|
|
1838
|
+
}
|
|
1839
|
+
if (!isSingleCardDocument(json)) {
|
|
1840
|
+
// The URL turned out to be a binary file (e.g. an uploaded
|
|
1841
|
+
// image). The realm-server returns a file-meta JSON document
|
|
1842
|
+
// in that case; reroute to the file-meta load path so the
|
|
1843
|
+
// caller gets a FileDef instead of a hard failure.
|
|
1844
|
+
if (isSingleFileMetaDocument(json)) {
|
|
1845
|
+
// URL was a binary file; reroute to the file-meta bucket.
|
|
1846
|
+
let fileMeta = await this.getFileMetaInstance<FileDef>({
|
|
1847
|
+
idOrDoc: url,
|
|
1848
|
+
opts: {
|
|
1849
|
+
noCache: opts?.noCache,
|
|
1850
|
+
dependencyTrackingContext: opts?.dependencyTrackingContext,
|
|
1851
|
+
},
|
|
1852
|
+
});
|
|
1853
|
+
// Resolve inflightGetCards so concurrent callers don't hang.
|
|
1854
|
+
deferred?.fulfill(fileMeta as unknown as T | CardErrorJSONAPI);
|
|
1855
|
+
return fileMeta as unknown as T;
|
|
1856
|
+
}
|
|
1857
|
+
throw new Error(
|
|
1858
|
+
`bug: server returned a non card document for ${url}:
|
|
1859
|
+
${JSON.stringify(json, null, 2)}`,
|
|
1860
|
+
);
|
|
1861
|
+
}
|
|
1862
|
+
if (!json.data.id) {
|
|
1863
|
+
// card source format is not serialized with the ID, so we add that back in.
|
|
1864
|
+
json.data.id = url as RealmResourceIdentifier;
|
|
1865
|
+
}
|
|
1866
|
+
if (!json.data.meta?.realmURL) {
|
|
1867
|
+
// Source-mode loads in render context don't include realm metadata.
|
|
1868
|
+
// Query-backed relationship fields require realmURL to build their
|
|
1869
|
+
// fallback search query.
|
|
1870
|
+
let realmURL = this.realm.realmOf(rri(url));
|
|
1871
|
+
if (realmURL) {
|
|
1872
|
+
json.data.meta = {
|
|
1873
|
+
...(json.data.meta ?? {}),
|
|
1874
|
+
realmURL: realmURL as RealmIdentifier,
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
doc = json;
|
|
1879
|
+
}
|
|
1880
|
+
let instance = await this.createFromSerialized(
|
|
1881
|
+
doc.data,
|
|
1882
|
+
doc,
|
|
1883
|
+
cardIdToURL(doc.data.id!), // instances from the server will have id's
|
|
1884
|
+
opts?.dependencyTrackingContext,
|
|
1885
|
+
);
|
|
1886
|
+
// in case the url is an alias for the id (like index card without the
|
|
1887
|
+
// "/index") we also add this
|
|
1888
|
+
this.store.setCard(url, instance);
|
|
1889
|
+
deferred?.fulfill(instance as T);
|
|
1890
|
+
if (!existingInstance || !isCardInstance(existingInstance)) {
|
|
1891
|
+
this.setIdentityContext(instance);
|
|
1892
|
+
await this.startAutoSaving(instance);
|
|
1893
|
+
}
|
|
1894
|
+
return instance as T;
|
|
1895
|
+
} catch (error: any) {
|
|
1896
|
+
let errorResponse = processCardError(id, error);
|
|
1897
|
+
let cardError = errorResponse.errors[0];
|
|
1898
|
+
deferred?.fulfill(cardError);
|
|
1899
|
+
this.setIdentityContext(cardError);
|
|
1900
|
+
let status = cardError?.status ?? error?.status;
|
|
1901
|
+
let isSystemCardDefault = isSystemCardDefaultId(
|
|
1902
|
+
id,
|
|
1903
|
+
idOrDoc,
|
|
1904
|
+
cardError?.id,
|
|
1905
|
+
);
|
|
1906
|
+
// suppress logging of 404s for system card defaults during tests
|
|
1907
|
+
let shouldLogAsError = !(
|
|
1908
|
+
isTesting() &&
|
|
1909
|
+
status === 404 &&
|
|
1910
|
+
isSystemCardDefault
|
|
1911
|
+
);
|
|
1912
|
+
let message = `error getting instance ${JSON.stringify(idOrDoc, null, 2)}: ${JSON.stringify(error, null, 2)}`;
|
|
1913
|
+
if (shouldLogAsError) {
|
|
1914
|
+
storeLogger.error(message, error);
|
|
1915
|
+
} else {
|
|
1916
|
+
storeLogger.debug(message, error);
|
|
1917
|
+
}
|
|
1918
|
+
return cardError;
|
|
1919
|
+
} finally {
|
|
1920
|
+
if (id) {
|
|
1921
|
+
this.inflightGetCards.delete(id);
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
private async getFileMetaInstance<T extends FileDef>({
|
|
1927
|
+
idOrDoc,
|
|
1928
|
+
opts,
|
|
1929
|
+
}: {
|
|
1930
|
+
idOrDoc: string | LooseSingleCardDocument;
|
|
1931
|
+
opts?: {
|
|
1932
|
+
noCache?: boolean;
|
|
1933
|
+
dependencyTrackingContext?: RuntimeDependencyTrackingContext;
|
|
1934
|
+
};
|
|
1935
|
+
}): Promise<T | CardErrorJSONAPI> {
|
|
1936
|
+
let deferred: Deferred<T | CardErrorJSONAPI> | undefined;
|
|
1937
|
+
let id = asURL(idOrDoc);
|
|
1938
|
+
if (!id) {
|
|
1939
|
+
throw new Error('file-meta reads require a URL id');
|
|
1940
|
+
}
|
|
1941
|
+
let working = this.inflightGetFileMeta.get(id);
|
|
1942
|
+
if (working) {
|
|
1943
|
+
return working as Promise<T | CardErrorJSONAPI>;
|
|
1944
|
+
}
|
|
1945
|
+
deferred = new Deferred<T | CardErrorJSONAPI>();
|
|
1946
|
+
this.inflightGetFileMeta.set(
|
|
1947
|
+
id,
|
|
1948
|
+
deferred.promise as Promise<FileDef | CardErrorJSONAPI>,
|
|
1949
|
+
);
|
|
1950
|
+
try {
|
|
1951
|
+
let existingInstance = this.peek(id, { type: 'file-meta' });
|
|
1952
|
+
if (!opts?.noCache && existingInstance) {
|
|
1953
|
+
deferred.fulfill(existingInstance as T | CardErrorJSONAPI);
|
|
1954
|
+
return existingInstance as T | CardErrorJSONAPI;
|
|
1955
|
+
}
|
|
1956
|
+
if (isLocalId(id) && !isRegisteredPrefix(id)) {
|
|
1957
|
+
throw new Error(`file-meta reads do not support local ids (${id})`);
|
|
1958
|
+
}
|
|
1959
|
+
let url = isRegisteredPrefix(id) ? cardIdToURL(id).href : id;
|
|
1960
|
+
let fileMetaDoc: SingleFileMetaDocument | CardError;
|
|
1961
|
+
if (this.isRenderStore && (globalThis as any).__boxelRenderContext) {
|
|
1962
|
+
fileMetaDoc = await this.extractFileMetaDirectly(url);
|
|
1963
|
+
} else {
|
|
1964
|
+
fileMetaDoc = await this.store.loadFileMetaDocument(url, {
|
|
1965
|
+
dependencyTrackingContext: opts?.dependencyTrackingContext,
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
if (isCardError(fileMetaDoc)) {
|
|
1969
|
+
throw fileMetaDoc;
|
|
1970
|
+
}
|
|
1971
|
+
let api = await this.cardService.getAPI();
|
|
1972
|
+
let fileInstance = await api.createFromSerialized(
|
|
1973
|
+
fileMetaDoc.data,
|
|
1974
|
+
fileMetaDoc,
|
|
1975
|
+
fileMetaDoc.data.id ? cardIdToURL(fileMetaDoc.data.id) : new URL(url),
|
|
1976
|
+
{
|
|
1977
|
+
store: this.store,
|
|
1978
|
+
dependencyTrackingContext: opts?.dependencyTrackingContext,
|
|
1979
|
+
},
|
|
1980
|
+
);
|
|
1981
|
+
this.setIdentityContext(fileInstance as unknown as FileDef, 'file-meta');
|
|
1982
|
+
deferred.fulfill(fileInstance as T);
|
|
1983
|
+
return fileInstance as T;
|
|
1984
|
+
} catch (error: any) {
|
|
1985
|
+
let errorResponse = processCardError(id, error);
|
|
1986
|
+
let cardError = errorResponse.errors[0];
|
|
1987
|
+
deferred.fulfill(cardError);
|
|
1988
|
+
console.error(
|
|
1989
|
+
`error getting file-meta instance ${JSON.stringify(idOrDoc, null, 2)}: ${JSON.stringify(error, null, 2)}`,
|
|
1990
|
+
error,
|
|
1991
|
+
);
|
|
1992
|
+
return cardError;
|
|
1993
|
+
} finally {
|
|
1994
|
+
this.inflightGetFileMeta.delete(id);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
private async extractFileMetaDirectly(
|
|
1999
|
+
url: string,
|
|
2000
|
+
): Promise<SingleFileMetaDocument | CardError> {
|
|
2001
|
+
let fileDefCodeRef = resolveFileDefCodeRef(new URL(url));
|
|
2002
|
+
let extractor = new FileDefAttributesExtractor({
|
|
2003
|
+
loaderService: this.loaderService,
|
|
2004
|
+
network: this.network,
|
|
2005
|
+
fileURL: url,
|
|
2006
|
+
fileDefCodeRef,
|
|
2007
|
+
baseFileDefCodeRef: baseFileRef,
|
|
2008
|
+
contentHash: undefined,
|
|
2009
|
+
contentSize: undefined,
|
|
2010
|
+
buildError: (errorUrl, error) => {
|
|
2011
|
+
let errorJSONAPI = formattedError(errorUrl, error).errors[0];
|
|
2012
|
+
return errorJsonApiToErrorEntry(errorJSONAPI) as RenderError;
|
|
2013
|
+
},
|
|
2014
|
+
});
|
|
2015
|
+
let result = await extractor.extract();
|
|
2016
|
+
if (result.status === 'error' || !result.resource) {
|
|
2017
|
+
let msg = result.error?.error?.message ?? 'File extract failed';
|
|
2018
|
+
return new CardError(msg, { status: 500 });
|
|
2019
|
+
}
|
|
2020
|
+
return { data: result.resource };
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
// this function is used to determine if the instance will be auto-saved or
|
|
2024
|
+
// note this is a temporary function that is likely to go away with the
|
|
2025
|
+
// creation of completion ephemeral state solution of the store/realm the
|
|
2026
|
+
// only use-case for this function is determining if a preview instance in
|
|
2027
|
+
// catalog realm (which is a read-only), st a card can be mutable without
|
|
2028
|
+
// persisting to the server
|
|
2029
|
+
private useEphemeralState(instance: CardDef | undefined): boolean {
|
|
2030
|
+
if (!instance) {
|
|
2031
|
+
return false;
|
|
2032
|
+
}
|
|
2033
|
+
let realmURL = instance[realmURLSymbol];
|
|
2034
|
+
if (!realmURL) {
|
|
2035
|
+
// if a proper cannot derived, I just revert to the default behavior of auto-save
|
|
2036
|
+
return false;
|
|
2037
|
+
}
|
|
2038
|
+
let permissionToWrite = this.realm.permissions(realmURL.href).canWrite;
|
|
2039
|
+
return !permissionToWrite;
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
private doAutoSave(
|
|
2043
|
+
idOrInstance: string | CardDef,
|
|
2044
|
+
opts?: { isImmediate?: true },
|
|
2045
|
+
) {
|
|
2046
|
+
let instance: CardDef | undefined;
|
|
2047
|
+
if (typeof idOrInstance === 'string') {
|
|
2048
|
+
let maybeInstance = this.peek(idOrInstance);
|
|
2049
|
+
if (!isCardInstance(maybeInstance)) {
|
|
2050
|
+
return;
|
|
2051
|
+
}
|
|
2052
|
+
instance = maybeInstance;
|
|
2053
|
+
} else {
|
|
2054
|
+
instance = idOrInstance;
|
|
2055
|
+
}
|
|
2056
|
+
if (this.useEphemeralState(instance)) {
|
|
2057
|
+
return;
|
|
2058
|
+
}
|
|
2059
|
+
let autoSaveState = this.initOrGetAutoSaveState(instance);
|
|
2060
|
+
let queueName = instance.id ?? instance[localIdSymbol];
|
|
2061
|
+
let autoSaveQueue = this.autoSaveQueues.get(queueName);
|
|
2062
|
+
if (!autoSaveQueue) {
|
|
2063
|
+
autoSaveQueue = [];
|
|
2064
|
+
this.autoSaveQueues.set(queueName, autoSaveQueue);
|
|
2065
|
+
}
|
|
2066
|
+
autoSaveQueue.push({ ...opts });
|
|
2067
|
+
autoSaveState.isSaving = true;
|
|
2068
|
+
autoSaveState.lastSaveError = undefined;
|
|
2069
|
+
this.drainAutoSaveQueue(queueName);
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
private async drainAutoSaveQueue(queueName: string) {
|
|
2073
|
+
return await this.withTestWaiters(async () => {
|
|
2074
|
+
await this.autoSavePromises.get(queueName);
|
|
2075
|
+
|
|
2076
|
+
let instance = this.peek(queueName);
|
|
2077
|
+
if (!isCardInstance(instance)) {
|
|
2078
|
+
return;
|
|
2079
|
+
}
|
|
2080
|
+
await this.inflightCardMutations.get(instance[localIdSymbol]);
|
|
2081
|
+
|
|
2082
|
+
let done: () => void;
|
|
2083
|
+
this.autoSavePromises.set(
|
|
2084
|
+
queueName,
|
|
2085
|
+
new Promise<void>((r) => (done = r)),
|
|
2086
|
+
);
|
|
2087
|
+
let autoSaves = [...(this.autoSaveQueues.get(queueName) ?? [])];
|
|
2088
|
+
this.autoSaveQueues.set(queueName, []);
|
|
2089
|
+
if (autoSaves && autoSaves.length > 0) {
|
|
2090
|
+
let autoSaveState = this.initOrGetAutoSaveState(instance);
|
|
2091
|
+
// favor isImmediate saves
|
|
2092
|
+
let isImmediate = Boolean(autoSaves.find((a) => a.isImmediate));
|
|
2093
|
+
try {
|
|
2094
|
+
let maybeError = await this.saveInstance(
|
|
2095
|
+
instance,
|
|
2096
|
+
isImmediate ? { isImmediate } : undefined,
|
|
2097
|
+
);
|
|
2098
|
+
autoSaveState.hasUnsavedChanges = false;
|
|
2099
|
+
autoSaveState.lastSaved = Date.now();
|
|
2100
|
+
autoSaveState.lastSavedErrorMsg = undefined;
|
|
2101
|
+
autoSaveState.lastSaveError =
|
|
2102
|
+
maybeError && !isCardInstance(maybeError) ? maybeError : undefined;
|
|
2103
|
+
} catch (error) {
|
|
2104
|
+
// error will already be logged in CardService
|
|
2105
|
+
if (autoSaveState) {
|
|
2106
|
+
autoSaveState.lastSaveError = error as Error;
|
|
2107
|
+
}
|
|
2108
|
+
} finally {
|
|
2109
|
+
autoSaveState.isSaving = false;
|
|
2110
|
+
this.calculateLastSavedMsg(autoSaveState);
|
|
2111
|
+
if (isLocalId(queueName) && instance.id) {
|
|
2112
|
+
this.autoSaveStates.set(instance.id, autoSaveState);
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
done!();
|
|
2117
|
+
});
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
private initOrGetAutoSaveState(instance: CardDef): AutoSaveState {
|
|
2121
|
+
let autoSaveState = this.autoSaveStates.get(
|
|
2122
|
+
instance.id ?? instance[localIdSymbol],
|
|
2123
|
+
);
|
|
2124
|
+
if (!autoSaveState) {
|
|
2125
|
+
autoSaveState = new TrackedObject({
|
|
2126
|
+
isSaving: false,
|
|
2127
|
+
hasUnsavedChanges: false,
|
|
2128
|
+
lastSaved: undefined,
|
|
2129
|
+
lastSavedErrorMsg: undefined,
|
|
2130
|
+
lastSaveError: undefined,
|
|
2131
|
+
});
|
|
2132
|
+
this.autoSaveStates.set(instance[localIdSymbol], autoSaveState);
|
|
2133
|
+
}
|
|
2134
|
+
if (instance.id && !this.autoSaveStates.get(instance.id)) {
|
|
2135
|
+
this.autoSaveStates.set(instance.id, autoSaveState);
|
|
2136
|
+
}
|
|
2137
|
+
return autoSaveState;
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
private async saveInstance(instance: CardDef, opts?: { isImmediate?: true }) {
|
|
2141
|
+
if (this.renderContextBlocksPersistence()) {
|
|
2142
|
+
// we skip saving when rendering cards in headless chrome
|
|
2143
|
+
return;
|
|
2144
|
+
}
|
|
2145
|
+
if (opts?.isImmediate) {
|
|
2146
|
+
return await this.persistAndUpdate(instance);
|
|
2147
|
+
} else {
|
|
2148
|
+
// these saves can happen so fast that we'll make sure to wait at
|
|
2149
|
+
// least 500ms for human consumption
|
|
2150
|
+
let [result] = await Promise.all([
|
|
2151
|
+
this.persistAndUpdate(instance),
|
|
2152
|
+
delay(500),
|
|
2153
|
+
]);
|
|
2154
|
+
return result;
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
private async saveCardDocument(
|
|
2159
|
+
doc: LooseSingleCardDocument,
|
|
2160
|
+
opts?: PersistOptions,
|
|
2161
|
+
): Promise<SingleCardDocument> {
|
|
2162
|
+
let isSaved = !!doc.data.id;
|
|
2163
|
+
let url = resolveDocUrl(doc.data.id, opts?.realm, opts?.localDir);
|
|
2164
|
+
let json = await this.cardService.fetchJSON(url, {
|
|
2165
|
+
method: isSaved ? 'PATCH' : 'POST',
|
|
2166
|
+
body: JSON.stringify(doc, null, 2),
|
|
2167
|
+
headers: {
|
|
2168
|
+
'Content-Type': SupportedMimeType.CardJson,
|
|
2169
|
+
},
|
|
2170
|
+
clientRequestId: opts?.clientRequestId,
|
|
2171
|
+
});
|
|
2172
|
+
if (!isSingleCardDocument(json)) {
|
|
2173
|
+
throw new Error(
|
|
2174
|
+
`bug: arg is not a card document:
|
|
2175
|
+
${JSON.stringify(json, null, 2)}`,
|
|
2176
|
+
);
|
|
2177
|
+
}
|
|
2178
|
+
return json;
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
private calculateLastSavedMsg(autoSaveState: AutoSaveState) {
|
|
2182
|
+
let savedMessage: string | undefined;
|
|
2183
|
+
if (autoSaveState.lastSaveError) {
|
|
2184
|
+
savedMessage = `Failed to save: ${this.getErrorMessage(
|
|
2185
|
+
autoSaveState.lastSaveError,
|
|
2186
|
+
)}`;
|
|
2187
|
+
} else if (autoSaveState.lastSaved) {
|
|
2188
|
+
savedMessage = `Saved ${formatDistanceToNow(autoSaveState.lastSaved, {
|
|
2189
|
+
addSuffix: true,
|
|
2190
|
+
})}`;
|
|
2191
|
+
}
|
|
2192
|
+
if (autoSaveState.lastSavedErrorMsg != savedMessage) {
|
|
2193
|
+
autoSaveState.lastSavedErrorMsg = savedMessage;
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
private getErrorMessage(error: CardErrorJSONAPI | Error) {
|
|
2198
|
+
if (
|
|
2199
|
+
'meta' in error &&
|
|
2200
|
+
typeof error.meta === 'object' &&
|
|
2201
|
+
'responseHeaders' in error.meta &&
|
|
2202
|
+
error.meta.responseHeaders &&
|
|
2203
|
+
typeof error.meta.responseHeaders === 'object'
|
|
2204
|
+
) {
|
|
2205
|
+
let wafRule = Object.entries(error.meta.responseHeaders).find(
|
|
2206
|
+
([header]) => header.toLowerCase() === 'x-blocked-by-waf-rule',
|
|
2207
|
+
)?.[1];
|
|
2208
|
+
if (wafRule) {
|
|
2209
|
+
return `Request blocked by Web Application Firewall. X-blocked-by-waf-rule response header specifies rule: ${wafRule}`;
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
if (error.message) {
|
|
2213
|
+
return error.message;
|
|
2214
|
+
}
|
|
2215
|
+
return 'Unknown error';
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
private async persistAndUpdate(
|
|
2219
|
+
instance: CardDef,
|
|
2220
|
+
opts?: PersistOptions,
|
|
2221
|
+
): Promise<CardDef | CardErrorJSONAPI> {
|
|
2222
|
+
return await this.withTestWaiters(async () => {
|
|
2223
|
+
let isNew = !instance.id;
|
|
2224
|
+
let inflightMutation = this.inflightCardMutations.get(
|
|
2225
|
+
instance[localIdSymbol],
|
|
2226
|
+
);
|
|
2227
|
+
if (inflightMutation) {
|
|
2228
|
+
// the local instance is always up-to-date, but things can get messy if
|
|
2229
|
+
// we try to update an instance that is in the process of being created on
|
|
2230
|
+
// the server, because then it still looks like to the client another
|
|
2231
|
+
// POST should be issued when instead we really want to PATCH.
|
|
2232
|
+
await inflightMutation;
|
|
2233
|
+
}
|
|
2234
|
+
let deferred = new Deferred<void>();
|
|
2235
|
+
this.inflightCardMutations.set(instance[localIdSymbol], deferred.promise);
|
|
2236
|
+
try {
|
|
2237
|
+
let doc = await this.cardService.serializeCard(instance, {
|
|
2238
|
+
// for a brand new card that has no id yet, we don't know what we are
|
|
2239
|
+
// relativeTo because its up to the realm server to assign us an ID, so
|
|
2240
|
+
// URL's should be absolute
|
|
2241
|
+
useAbsoluteURL: true,
|
|
2242
|
+
withIncluded: true,
|
|
2243
|
+
omitQueryFields: true,
|
|
2244
|
+
});
|
|
2245
|
+
|
|
2246
|
+
// send doc over the wire with absolute URL's. The realm server will convert
|
|
2247
|
+
// to relative URL's as it serializes the cards
|
|
2248
|
+
let realmURL = instance[realmURLSymbol];
|
|
2249
|
+
// in the case where we get no realm URL from the card, we are dealing with
|
|
2250
|
+
// a new card instance that does not have a realm URL yet.
|
|
2251
|
+
if (!realmURL) {
|
|
2252
|
+
let defaultRealmHref =
|
|
2253
|
+
opts?.realm ?? this.realm.defaultWritableRealm?.path;
|
|
2254
|
+
if (!defaultRealmHref) {
|
|
2255
|
+
throw new Error('Could not find a writable realm');
|
|
2256
|
+
}
|
|
2257
|
+
realmURL = new URL(defaultRealmHref);
|
|
2258
|
+
}
|
|
2259
|
+
let json = await this.saveCardDocument(doc, {
|
|
2260
|
+
realm: realmURL.href,
|
|
2261
|
+
localDir: opts?.localDir,
|
|
2262
|
+
clientRequestId: opts?.clientRequestId,
|
|
2263
|
+
});
|
|
2264
|
+
|
|
2265
|
+
let api = await this.cardService.getAPI();
|
|
2266
|
+
// the store state represents the latest state and the server state is
|
|
2267
|
+
// potentially out-of-date. As such we only merge the server state that
|
|
2268
|
+
// the store does not know about specifically remote ID's and realm
|
|
2269
|
+
// meta. the attributes and relationships state from the server are
|
|
2270
|
+
// thrown away since the store has a more recent version of these.
|
|
2271
|
+
if (needsServerStateMerge(instance, json)) {
|
|
2272
|
+
let serverState = cloneDeep(json);
|
|
2273
|
+
delete serverState.data.attributes;
|
|
2274
|
+
delete serverState.data.relationships;
|
|
2275
|
+
await api.updateFromSerialized(instance, serverState, this.store);
|
|
2276
|
+
}
|
|
2277
|
+
if (isNew) {
|
|
2278
|
+
api.setId(instance, json.data.id!);
|
|
2279
|
+
this.subscribeToRealm(rri(instance.id));
|
|
2280
|
+
this.operatorModeStateService.handleCardIdAssignment(
|
|
2281
|
+
instance[localIdSymbol],
|
|
2282
|
+
);
|
|
2283
|
+
await this.updateForeignConsumersOf(instance);
|
|
2284
|
+
this.setIdentityContext(instance);
|
|
2285
|
+
await this.startAutoSaving(instance);
|
|
2286
|
+
}
|
|
2287
|
+
if (this.onSaveSubscriber) {
|
|
2288
|
+
this.onSaveSubscriber(cardIdToURL(json.data.id!), json);
|
|
2289
|
+
}
|
|
2290
|
+
return instance;
|
|
2291
|
+
} catch (err) {
|
|
2292
|
+
console.error(`Failed to save ${instance.id}: `, err);
|
|
2293
|
+
let errorResponse = processCardError(
|
|
2294
|
+
instance.id ?? instance[localIdSymbol],
|
|
2295
|
+
err,
|
|
2296
|
+
);
|
|
2297
|
+
let cardError = errorResponse.errors[0];
|
|
2298
|
+
this.setIdentityContext(cardError);
|
|
2299
|
+
let remoteId = cardError.meta?.remoteId;
|
|
2300
|
+
if (remoteId && (!cardError.id || isLocalId(cardError.id))) {
|
|
2301
|
+
this.store.addCardInstanceOrError(remoteId, cardError);
|
|
2302
|
+
}
|
|
2303
|
+
return cardError;
|
|
2304
|
+
} finally {
|
|
2305
|
+
deferred.fulfill();
|
|
2306
|
+
}
|
|
2307
|
+
});
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
// in the case we are making a cross realm relationship with a link that
|
|
2311
|
+
// hasn't been saved yet, as soon as the link does actually get saved we need
|
|
2312
|
+
// to inform the consuming instances that live in different realms of the new
|
|
2313
|
+
// link's remote id and have those consumers update in their respective
|
|
2314
|
+
// realms.
|
|
2315
|
+
private async updateForeignConsumersOf(instance: CardDef) {
|
|
2316
|
+
let consumers = this.store.consumersOf(
|
|
2317
|
+
await this.cardService.getAPI(),
|
|
2318
|
+
instance,
|
|
2319
|
+
);
|
|
2320
|
+
let instanceRealm = instance[realmURLSymbol]?.href;
|
|
2321
|
+
if (!instanceRealm) {
|
|
2322
|
+
return;
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
for (let consumer of consumers) {
|
|
2326
|
+
let consumerRealm = consumer[realmURLSymbol]?.href;
|
|
2327
|
+
if (consumerRealm !== instanceRealm && consumer.id) {
|
|
2328
|
+
this.save(consumer.id);
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
private async reloadInstance(instance: CardDef): Promise<void> {
|
|
2334
|
+
// we don't await this in the realm subscription callback, so this test
|
|
2335
|
+
// waiter should catch otherwise leaky async in the tests
|
|
2336
|
+
await this.withTestWaiters(async () => {
|
|
2337
|
+
let api = await this.cardService.getAPI();
|
|
2338
|
+
let incomingDoc: SingleCardDocument = (await this.cardService.fetchJSON(
|
|
2339
|
+
instance.id,
|
|
2340
|
+
undefined,
|
|
2341
|
+
)) as SingleCardDocument;
|
|
2342
|
+
|
|
2343
|
+
if (!isSingleCardDocument(incomingDoc)) {
|
|
2344
|
+
throw new Error(
|
|
2345
|
+
`bug: server returned a non card document for ${instance.id}:
|
|
2346
|
+
${JSON.stringify(incomingDoc, null, 2)}`,
|
|
2347
|
+
);
|
|
2348
|
+
}
|
|
2349
|
+
await api.updateFromSerialized<typeof CardDef>(
|
|
2350
|
+
instance,
|
|
2351
|
+
incomingDoc,
|
|
2352
|
+
this.store,
|
|
2353
|
+
);
|
|
2354
|
+
});
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
private subscribeToRealm(url: RealmResourceIdentifier | URL) {
|
|
2358
|
+
if (this.hostModeService.isActive) {
|
|
2359
|
+
return;
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
let realmURL = this.realm.realmOf(url);
|
|
2363
|
+
if (!realmURL) {
|
|
2364
|
+
console.warn(
|
|
2365
|
+
`could not determine realm for card ${url instanceof URL ? url.href : url} when trying to subscribe to realm`,
|
|
2366
|
+
);
|
|
2367
|
+
return;
|
|
2368
|
+
}
|
|
2369
|
+
let subscription = this.subscriptions.get(realmURL);
|
|
2370
|
+
if (!subscription) {
|
|
2371
|
+
this.subscriptions.set(realmURL, {
|
|
2372
|
+
unsubscribe: this.messageService.subscribe(realmURL, (event) =>
|
|
2373
|
+
this.handleInvalidations(event),
|
|
2374
|
+
),
|
|
2375
|
+
});
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
private async loadPatchedInstances(
|
|
2380
|
+
patchData: PatchData,
|
|
2381
|
+
relativeTo: URL | undefined,
|
|
2382
|
+
): Promise<{
|
|
2383
|
+
[fieldName: string]: CardDef | CardDef[];
|
|
2384
|
+
}> {
|
|
2385
|
+
if (!patchData?.relationships) {
|
|
2386
|
+
return {};
|
|
2387
|
+
}
|
|
2388
|
+
let result: { [fieldName: string]: CardDef | CardDef[] } = {};
|
|
2389
|
+
await Promise.all(
|
|
2390
|
+
Object.entries(patchData.relationships).map(async ([fieldName, rel]) => {
|
|
2391
|
+
if (Array.isArray(rel)) {
|
|
2392
|
+
let instances: CardDef[] = [];
|
|
2393
|
+
await Promise.all(
|
|
2394
|
+
rel.map(async (r) => {
|
|
2395
|
+
let instance = await this.loadRelationshipInstance(r, relativeTo);
|
|
2396
|
+
if (instance) {
|
|
2397
|
+
instances.push(instance);
|
|
2398
|
+
}
|
|
2399
|
+
}),
|
|
2400
|
+
);
|
|
2401
|
+
result[fieldName] = instances;
|
|
2402
|
+
} else {
|
|
2403
|
+
let instance = await this.loadRelationshipInstance(rel, relativeTo);
|
|
2404
|
+
if (instance) {
|
|
2405
|
+
result[fieldName] = instance;
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
}),
|
|
2409
|
+
);
|
|
2410
|
+
return result;
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
private async loadRelationshipInstance(
|
|
2414
|
+
rel: Relationship,
|
|
2415
|
+
relativeTo: URL | undefined,
|
|
2416
|
+
) {
|
|
2417
|
+
if (!rel.links?.self) {
|
|
2418
|
+
return;
|
|
2419
|
+
}
|
|
2420
|
+
let id = rel.links.self;
|
|
2421
|
+
let instance = await this.getCardInstance({
|
|
2422
|
+
idOrDoc: resolveCardReference(id, relativeTo),
|
|
2423
|
+
});
|
|
2424
|
+
return isCardInstance(instance) ? instance : undefined;
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
private async withTestWaiters<T>(cb: () => Promise<T>) {
|
|
2428
|
+
let token = waiter.beginAsync();
|
|
2429
|
+
try {
|
|
2430
|
+
let result = await cb();
|
|
2431
|
+
// only do this in test env--this makes sure that we also wait for any
|
|
2432
|
+
// interior card instance async as part of our ember-test-waiters
|
|
2433
|
+
if (isTesting()) {
|
|
2434
|
+
await this.cardService.cardsSettled();
|
|
2435
|
+
}
|
|
2436
|
+
return result;
|
|
2437
|
+
} finally {
|
|
2438
|
+
waiter.endAsync(token);
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
function processCardError(
|
|
2444
|
+
url: string | undefined,
|
|
2445
|
+
error: any,
|
|
2446
|
+
): CardErrorsJSONAPI {
|
|
2447
|
+
try {
|
|
2448
|
+
let errorResponse = JSON.parse(error.responseText);
|
|
2449
|
+
return formattedError(url, error, errorResponse.errors?.[0]);
|
|
2450
|
+
} catch (parseError) {
|
|
2451
|
+
switch (error.status) {
|
|
2452
|
+
// tailor HTTP responses as necessary for better user feedback
|
|
2453
|
+
case 404:
|
|
2454
|
+
return formattedError(url, error, {
|
|
2455
|
+
status: 404,
|
|
2456
|
+
title: 'Card Not Found',
|
|
2457
|
+
message: `The card ${url} does not exist`,
|
|
2458
|
+
});
|
|
2459
|
+
default:
|
|
2460
|
+
return formattedError(url, error, undefined);
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
function needsServerStateMerge(
|
|
2466
|
+
instance: CardDef,
|
|
2467
|
+
serverState: SingleCardDocument,
|
|
2468
|
+
): boolean {
|
|
2469
|
+
return (
|
|
2470
|
+
instance.id !== serverState.data.id ||
|
|
2471
|
+
!isEqual(instance[meta]?.realmInfo, serverState.data.meta.realmInfo)
|
|
2472
|
+
);
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
export function asURL(urlOrDoc: string): string;
|
|
2476
|
+
export function asURL(urlOrDoc: LooseSingleCardDocument): string | undefined;
|
|
2477
|
+
export function asURL(
|
|
2478
|
+
urlOrDoc: string | LooseSingleCardDocument,
|
|
2479
|
+
): string | undefined;
|
|
2480
|
+
export function asURL(urlOrDoc: string | LooseSingleCardDocument) {
|
|
2481
|
+
if (typeof urlOrDoc !== 'string') {
|
|
2482
|
+
return urlOrDoc.data.id;
|
|
2483
|
+
}
|
|
2484
|
+
let id = urlOrDoc.replace(/\.json$/, '');
|
|
2485
|
+
return isLocalId(id) ? id : resolveCardReference(id, undefined);
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
function isSystemCardDefaultId(
|
|
2489
|
+
id: string | undefined,
|
|
2490
|
+
idOrDoc: string | LooseSingleCardDocument,
|
|
2491
|
+
errorId: string | undefined,
|
|
2492
|
+
): boolean {
|
|
2493
|
+
let candidates = [
|
|
2494
|
+
id,
|
|
2495
|
+
typeof idOrDoc === 'string' ? idOrDoc : idOrDoc?.data?.id,
|
|
2496
|
+
errorId,
|
|
2497
|
+
].filter(Boolean) as string[];
|
|
2498
|
+
return candidates.some((candidate) =>
|
|
2499
|
+
candidate.includes('/SystemCard/default'),
|
|
2500
|
+
);
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
async function withStubbedRenderTimers<T>(cb: () => Promise<T>): Promise<T> {
|
|
2504
|
+
if (typeof window === 'undefined' || isTesting()) {
|
|
2505
|
+
return await cb();
|
|
2506
|
+
}
|
|
2507
|
+
// Prevent cards that use timers (e.g. timers-card.gts) from continuing to
|
|
2508
|
+
// execute after we capture their HTML during prerender. In the browser we
|
|
2509
|
+
// normally let timers run, but in the render route we need deterministic,
|
|
2510
|
+
// single-shot renders so runaway timers don't crash indexing.
|
|
2511
|
+
let restore = enableRenderTimerStub();
|
|
2512
|
+
try {
|
|
2513
|
+
return await withTimersBlocked(cb);
|
|
2514
|
+
} finally {
|
|
2515
|
+
restore();
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
// Resolves either to
|
|
2520
|
+
// - an instance
|
|
2521
|
+
// - a directory
|
|
2522
|
+
function resolveDocUrl(id?: string, realm?: string, local?: string) {
|
|
2523
|
+
if (id) {
|
|
2524
|
+
return id;
|
|
2525
|
+
}
|
|
2526
|
+
if (!realm) {
|
|
2527
|
+
throw new Error('Cannot resolve target url without a realm');
|
|
2528
|
+
}
|
|
2529
|
+
let path = new RealmPaths(new URL(realm));
|
|
2530
|
+
if (local) {
|
|
2531
|
+
return path.directoryURL(local).href;
|
|
2532
|
+
}
|
|
2533
|
+
return path.url;
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
declare module '@ember/service' {
|
|
2537
|
+
interface Registry {
|
|
2538
|
+
store: StoreService;
|
|
2539
|
+
}
|
|
2540
|
+
}
|