@antelopejs/dms 0.9.8 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +171 -0
- package/README.md +1 -1
- package/data/module-store/catalog.json +2 -2
- package/dist/attachments/table-view.js +2 -2
- package/dist/attachments/table-view.js.map +1 -1
- package/dist/automation/actions.d.ts +2 -0
- package/dist/automation/actions.js +116 -71
- package/dist/automation/actions.js.map +1 -1
- package/dist/automation/i18n.d.ts +4 -0
- package/dist/automation/i18n.js +19 -0
- package/dist/automation/i18n.js.map +1 -0
- package/dist/automation/send-notification.d.ts +21 -0
- package/dist/automation/send-notification.js +163 -0
- package/dist/automation/send-notification.js.map +1 -0
- package/dist/automation/triggers.js +19 -25
- package/dist/automation/triggers.js.map +1 -1
- package/dist/client-origin-cors.d.ts +1 -0
- package/dist/client-origin-cors.js +19 -0
- package/dist/client-origin-cors.js.map +1 -0
- package/dist/config.d.ts +3 -2
- package/dist/config.js +5 -7
- package/dist/config.js.map +1 -1
- package/dist/frontend-module.d.ts +4 -0
- package/dist/frontend-module.js +37 -0
- package/dist/frontend-module.js.map +1 -0
- package/dist/implementations/dms/client-ip.js +1 -1
- package/dist/implementations/dms/client-ip.js.map +1 -1
- package/dist/implementations/dms/dev-reload.d.ts +5 -0
- package/dist/implementations/dms/dev-reload.js +23 -3
- package/dist/implementations/dms/dev-reload.js.map +1 -1
- package/dist/implementations/dms/frontend-modules.d.ts +2 -4
- package/dist/implementations/dms/frontend-modules.js +38 -94
- package/dist/implementations/dms/frontend-modules.js.map +1 -1
- package/dist/implementations/dms/frontend-ready.d.ts +12 -2
- package/dist/implementations/dms/frontend-ready.js +34 -4
- package/dist/implementations/dms/frontend-ready.js.map +1 -1
- package/dist/implementations/dms/layout-banners.d.ts +2 -1
- package/dist/implementations/dms/layout-banners.js +9 -0
- package/dist/implementations/dms/layout-banners.js.map +1 -1
- package/dist/implementations/dms/manifest.d.ts +7 -2
- package/dist/implementations/dms/manifest.js +12 -7
- package/dist/implementations/dms/manifest.js.map +1 -1
- package/dist/implementations/dms/page.d.ts +4 -3
- package/dist/implementations/dms/page.js +49 -18
- package/dist/implementations/dms/page.js.map +1 -1
- package/dist/implementations/dms/permissions-resolver.d.ts +2 -2
- package/dist/implementations/dms/permissions-resolver.js +2 -2
- package/dist/implementations/dms/permissions-resolver.js.map +1 -1
- package/dist/implementations/dms/realtime.js +1 -0
- package/dist/implementations/dms/realtime.js.map +1 -1
- package/dist/implementations/dms-base/bulk-selection.d.ts +1 -1
- package/dist/implementations/dms-base/bulk-selection.js +23 -1
- package/dist/implementations/dms-base/bulk-selection.js.map +1 -1
- package/dist/implementations/dms-base/filter-tokens.js +5 -3
- package/dist/implementations/dms-base/filter-tokens.js.map +1 -1
- package/dist/implementations/dms-base/search-route.d.ts +1 -0
- package/dist/implementations/dms-base/search-route.js +35 -13
- package/dist/implementations/dms-base/search-route.js.map +1 -1
- package/dist/implementations/dms-base/table-view.js +11 -6
- package/dist/implementations/dms-base/table-view.js.map +1 -1
- package/dist/implementations/dms-notifications/index.d.ts +2 -2
- package/dist/implementations/dms-notifications/index.js +5 -5
- package/dist/implementations/dms-notifications/index.js.map +1 -1
- package/dist/index.js +32 -55
- package/dist/index.js.map +1 -1
- package/dist/pages/modules/index.d.ts +1 -0
- package/dist/pages/modules/index.js +1 -0
- package/dist/pages/modules/index.js.map +1 -1
- package/dist/pages/modules/modules-store.d.ts +9 -0
- package/dist/pages/modules/modules-store.js +30 -0
- package/dist/pages/modules/modules-store.js.map +1 -0
- package/dist/pages/settings/users/account-credentials.js +2 -2
- package/dist/pages/settings/users/account-credentials.js.map +1 -1
- package/dist/pages/settings/users/account-data-store.js +25 -8
- package/dist/pages/settings/users/account-data-store.js.map +1 -1
- package/dist/pages/settings/users/invite-extension-routes.js +9 -3
- package/dist/pages/settings/users/invite-extension-routes.js.map +1 -1
- package/dist/pages/settings/users/member-management.js +13 -14
- package/dist/pages/settings/users/member-management.js.map +1 -1
- package/dist/pages/settings/users/profile.d.ts +1 -4
- package/dist/pages/settings/users/profile.js +0 -35
- package/dist/pages/settings/users/profile.js.map +1 -1
- package/dist/pages/settings/users/role-editor-store.js +26 -18
- package/dist/pages/settings/users/role-editor-store.js.map +1 -1
- package/dist/pages/settings/users/role-editor.d.ts +0 -1
- package/dist/pages/settings/users/role-editor.js +1 -2
- package/dist/pages/settings/users/role-editor.js.map +1 -1
- package/dist/pages/settings/users/role-preview.js +1 -2
- package/dist/pages/settings/users/role-preview.js.map +1 -1
- package/dist/pages/settings/users/roles.js +2 -2
- package/dist/pages/settings/users/roles.js.map +1 -1
- package/dist/pages/settings/users/two-factor-operations.js.map +1 -1
- package/dist/realtime/broker.d.ts +6 -0
- package/dist/realtime/broker.js +8 -1
- package/dist/realtime/broker.js.map +1 -1
- package/dist/realtime/presence.d.ts +8 -3
- package/dist/realtime/presence.js +8 -4
- package/dist/realtime/presence.js.map +1 -1
- package/dist/realtime/sessions.d.ts +11 -1
- package/dist/realtime/sessions.js +21 -3
- package/dist/realtime/sessions.js.map +1 -1
- package/dist/realtime/table-view.d.ts +2 -2
- package/dist/realtime/table-view.js +4 -3
- package/dist/realtime/table-view.js.map +1 -1
- package/dist/routes/auth/client-ip.d.ts +1 -0
- package/dist/routes/auth/client-ip.js +18 -0
- package/dist/routes/auth/client-ip.js.map +1 -1
- package/dist/routes/auth/oauth/config.d.ts +2 -1
- package/dist/routes/auth/oauth/config.js +5 -1
- package/dist/routes/auth/oauth/config.js.map +1 -1
- package/dist/routes/auth/session-handoff.d.ts +0 -1
- package/dist/routes/auth/session-handoff.js +0 -2
- package/dist/routes/auth/session-handoff.js.map +1 -1
- package/dist/routes/auth.js +3 -2
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/index.d.ts +0 -1
- package/dist/routes/index.js +0 -1
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/realtime.d.ts +13 -1
- package/dist/routes/realtime.js +114 -29
- package/dist/routes/realtime.js.map +1 -1
- package/dist/startup-config.d.ts +2 -0
- package/dist/startup-config.js +62 -0
- package/dist/startup-config.js.map +1 -0
- package/dist/store/interfaces.d.ts +3 -2
- package/dist/store/interfaces.js +16 -6
- package/dist/store/interfaces.js.map +1 -1
- package/dist/store/listing.js +25 -15
- package/dist/store/listing.js.map +1 -1
- package/dist/store/operations.js +5 -2
- package/dist/store/operations.js.map +1 -1
- package/dist/store/requirements.js +3 -2
- package/dist/store/requirements.js.map +1 -1
- package/dist/store/runtime.d.ts +10 -1
- package/dist/store/runtime.js +33 -6
- package/dist/store/runtime.js.map +1 -1
- package/dist/store/types.d.ts +10 -0
- package/dist/utils/ensure-default-tenant.d.ts +2 -1
- package/dist/utils/ensure-default-tenant.js +13 -1
- package/dist/utils/ensure-default-tenant.js.map +1 -1
- package/dist/utils/export-jobs.js +2 -1
- package/dist/utils/export-jobs.js.map +1 -1
- package/dist/validation/auth.schema.d.ts +2 -2
- package/dist/validation/member-invite.schema.d.ts +2 -10
- package/dist/validation/member-invite.schema.js +2 -15
- package/dist/validation/member-invite.schema.js.map +1 -1
- package/frontend-vue/dms.frontend.ts +24 -5
- package/frontend-vue/dms.server.ts +17 -0
- package/frontend-vue/layers/dms-auth/server/accounts.ts +3 -4
- package/frontend-vue/layers/dms-core/app/build/composables/renderNow.ts +33 -0
- package/frontend-vue/layers/dms-core/app/build/composables/user/tablePreferences.ts +5 -1
- package/frontend-vue/layers/dms-core/app/build/utils/crossTab.ts +199 -0
- package/frontend-vue/layers/dms-core/app/build/utils/sharedRealtimeConnection.ts +410 -0
- package/frontend-vue/layers/dms-core/app/composables/auth/useApiData.ts +77 -12
- package/frontend-vue/layers/dms-core/app/composables/data-types/useDataType.ts +26 -0
- package/frontend-vue/layers/dms-core/app/composables/live/livePauseSource.ts +29 -0
- package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +150 -9
- package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +6 -4
- package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +27 -1
- package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +256 -119
- package/frontend-vue/layers/dms-core/app/composables/translation/useComposedText.ts +21 -8
- package/frontend-vue/layers/dms-core/app/composables/translation/useTranslation.ts +25 -5
- package/frontend-vue/layers/dms-core/app/composables/useApiError.ts +3 -5
- package/frontend-vue/layers/dms-core/app/composables/useFieldErrors.ts +61 -26
- package/frontend-vue/layers/dms-core/app/plugins/dev-error-log.ts +16 -0
- package/frontend-vue/layers/dms-core/app/types/user.ts +6 -0
- package/frontend-vue/layers/dms-core/app/utils/composedText.ts +7 -2
- package/frontend-vue/layers/dms-core/app/utils/formatter.ts +134 -35
- package/frontend-vue/layers/dms-core/app/utils/url-interpolation.ts +18 -1
- package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +136 -2
- package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +136 -2
- package/frontend-vue/layers/dms-core/shared/types/app-config.ts +0 -2
- package/frontend-vue/layers/dms-layout/app/app.config.ts +0 -1
- package/frontend-vue/layers/dms-layout/app/assets/css/main.css +11 -1
- package/frontend-vue/layers/dms-layout/app/assets/fonts/Geist-Italic-Variable.woff2 +0 -0
- package/frontend-vue/layers/dms-layout/app/assets/fonts/Geist-Variable.woff2 +0 -0
- package/frontend-vue/layers/dms-layout/app/assets/fonts/GeistMono-Italic-Variable.woff2 +0 -0
- package/frontend-vue/layers/dms-layout/app/assets/fonts/GeistMono-Variable.woff2 +0 -0
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardBanners.vue +3 -2
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardHeader.vue +41 -21
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardPageBody.vue +6 -2
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidebar.vue +4 -19
- package/frontend-vue/layers/dms-layout/app/build/components/layout/Footer.vue +11 -107
- package/frontend-vue/layers/dms-layout/app/build/components/layout/GithubStarPrompt.vue +18 -7
- package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionsOutlet.vue +4 -0
- package/frontend-vue/layers/dms-layout/app/build/components/layout/RowSidePanel.vue +43 -82
- package/frontend-vue/layers/dms-layout/app/build/components/layout/SidebarUserMenu.vue +44 -3
- package/frontend-vue/layers/dms-layout/app/build/components/layout/UserIdentity.vue +11 -3
- package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleCatalogTile.vue +132 -35
- package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleManageMenu.vue +59 -0
- package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleSourceLabel.vue +25 -0
- package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreBanners.vue +73 -0
- package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreOperation.vue +116 -0
- package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreRequirements.vue +8 -3
- package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleStoreTile.vue +85 -207
- package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleTile.vue +10 -3
- package/frontend-vue/layers/dms-layout/app/build/components/pages/modules/ModuleTileSkeleton.vue +68 -0
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxDisplay.vue +24 -8
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxItem.vue +8 -5
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/region/RegionTimeZoneInput.vue +1 -0
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/role-permissions.ts +19 -0
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/useRoleEditor.ts +2 -1
- package/frontend-vue/layers/dms-layout/app/build/composables/layout/useSidePanelWidth.ts +31 -70
- package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationGroupsOpen.ts +11 -1
- package/frontend-vue/layers/dms-layout/app/build/utils/dev-reload-stream.ts +81 -0
- package/frontend-vue/layers/dms-layout/app/build/utils/github-repository.ts +2 -2
- package/frontend-vue/layers/dms-layout/app/build/utils/module-store-catalog.ts +232 -36
- package/frontend-vue/layers/dms-layout/app/build/utils/modules-catalog.ts +122 -14
- package/frontend-vue/layers/dms-layout/app/composables/layout/useLayoutBanners.ts +32 -3
- package/frontend-vue/layers/dms-layout/app/composables/layout/useServedHeaderActions.ts +49 -11
- package/frontend-vue/layers/dms-layout/app/composables/page/useModuleHistory.ts +3 -2
- package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStore.ts +59 -4
- package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStoreActions.ts +194 -0
- package/frontend-vue/layers/dms-layout/app/composables/page/usePageComponentPreload.ts +23 -3
- package/frontend-vue/layers/dms-layout/app/composables/page/usePageLayout.ts +4 -2
- package/frontend-vue/layers/dms-layout/app/composables/page/usePageRecordDeletion.ts +162 -0
- package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +6 -1
- package/frontend-vue/layers/dms-layout/app/custom-layouts/DefaultLayout.vue +6 -2
- package/frontend-vue/layers/dms-layout/app/custom-layouts/SettingsLayout.vue +6 -2
- package/frontend-vue/layers/dms-layout/app/custom-pages/modules-store.vue +291 -0
- package/frontend-vue/layers/dms-layout/app/custom-pages/modules.vue +87 -474
- package/frontend-vue/layers/dms-layout/app/middleware/module-routing.global.ts +3 -0
- package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +13 -22
- package/frontend-vue/layers/dms-layout/app/plugins/command-palette-navigation.client.ts +11 -10
- package/frontend-vue/layers/dms-layout/app/plugins/command-palette-personal-pages.client.ts +22 -52
- package/frontend-vue/layers/dms-layout/app/plugins/command-palette-quick-actions.client.ts +7 -7
- package/frontend-vue/layers/dms-layout/app/plugins/command-palette-session.client.ts +11 -10
- package/frontend-vue/layers/dms-layout/app/plugins/dms-dev-reload.client.ts +14 -9
- package/frontend-vue/layers/dms-layout/app/theme/actions.ts +5 -4
- package/frontend-vue/layers/dms-layout/app/theme/form-controls.ts +9 -4
- package/frontend-vue/layers/dms-layout/app/theme/surfaces.ts +22 -6
- package/frontend-vue/layers/dms-layout/app/types/module-store.ts +9 -0
- package/frontend-vue/layers/dms-layout/app/types/page.ts +15 -1
- package/frontend-vue/layers/dms-layout/app/utils/account-menu.ts +11 -0
- package/frontend-vue/layers/dms-layout/app/utils/breadcrumb.ts +57 -0
- package/frontend-vue/layers/dms-layout/app/utils/pageQuery.ts +26 -0
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +45 -18
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +44 -17
- package/frontend-vue/layers/dms-ui/app/build/components/containers/TargetComponentTree.vue +31 -0
- package/frontend-vue/layers/dms-ui/app/build/components/containers/constants.ts +25 -0
- package/frontend-vue/layers/dms-ui/app/build/components/containers/drawer/DynamicDrawer.vue +244 -15
- package/frontend-vue/layers/dms-ui/app/build/components/containers/modal/DynamicModal.vue +42 -25
- package/frontend-vue/layers/dms-ui/app/build/components/containers/row/RowHeader.vue +153 -0
- package/frontend-vue/layers/dms-ui/app/build/components/containers/row/RowNotFound.vue +44 -0
- package/frontend-vue/layers/dms-ui/app/build/components/containers/row/drawerRowHeader.ts +35 -0
- package/frontend-vue/layers/dms-ui/app/build/components/form/FormEntries.vue +44 -9
- package/frontend-vue/layers/dms-ui/app/build/components/form/FormErrorSummary.vue +11 -4
- package/frontend-vue/layers/dms-ui/app/build/components/form/FormFieldControl.vue +38 -5
- package/frontend-vue/layers/dms-ui/app/build/components/form/FormFieldNotes.vue +29 -5
- package/frontend-vue/layers/dms-ui/app/build/components/form/FormLoadError.vue +5 -1
- package/frontend-vue/layers/dms-ui/app/build/components/form/FormSectionNav.vue +3 -1
- package/frontend-vue/layers/dms-ui/app/build/components/form/FormSections.vue +106 -27
- package/frontend-vue/layers/dms-ui/app/build/components/table/Actions.vue +12 -4
- package/frontend-vue/layers/dms-ui/app/build/components/table/Empty.vue +4 -2
- package/frontend-vue/layers/dms-ui/app/build/components/table/FiltersRow.vue +48 -1
- package/frontend-vue/layers/dms-ui/app/build/components/table/Pagination.vue +55 -54
- package/frontend-vue/layers/dms-ui/app/build/components/table/RowSelection.vue +2 -1
- package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +396 -288
- package/frontend-vue/layers/dms-ui/app/build/components/table/Tabs.vue +2 -1
- package/frontend-vue/layers/dms-ui/app/build/components/table/Views.vue +2 -1
- package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardAvatar.vue +59 -0
- package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardGridSkeleton.vue +19 -2
- package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardRowActions.vue +38 -0
- package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardsDisplay.vue +148 -104
- package/frontend-vue/layers/dms-ui/app/build/components/table-view/LiveHoldBanner.vue +17 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/actions/bulkSelection.ts +43 -9
- package/frontend-vue/layers/dms-ui/app/build/composables/actions/rowComponentOptions.ts +76 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/actions/rowNavigation.ts +172 -27
- package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +270 -46
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/actionError.ts +6 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/confirmDialogTexts.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/containers/closingLayers.ts +186 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/containers/context.ts +14 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/containers/drawerSize.ts +229 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/containers/useResizableSize.ts +175 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/metricCells.ts +18 -35
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/numberSuffix.ts +44 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +123 -27
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerFieldTypes.ts +2 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationFilter.ts +75 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationRows.ts +92 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationValue.ts +39 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/twoLineCell.ts +3 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/useColumnValueRenderer.ts +6 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/form/createFormFields.ts +15 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/form/fieldSaved.ts +40 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/form/fieldValueRule.ts +62 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formEntryContext.ts +5 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formFooter.ts +42 -7
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formLayout.ts +106 -4
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formRecordLoad.ts +11 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formSections.ts +32 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formValidation.ts +36 -4
- package/frontend-vue/layers/dms-ui/app/build/composables/form/readonlyValue.ts +19 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/form/registerFormFunctions.ts +27 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/form/repeaterTable.ts +78 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/form/serverFieldErrors.ts +49 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/form/toasterClearance.ts +118 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/form/useInstantForm.ts +12 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/live/currentPageLive.ts +41 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/live/livePoller.ts +10 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/live/pageReadClock.ts +28 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/shortcuts.ts +1 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageLiveHost.ts +17 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/live/useUpdatedLabel.ts +38 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/page-toolbar/pageToolbarContext.ts +75 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/page-toolbar/toolbarControlHeight.ts +29 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/period/periodComparison.ts +52 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +21 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/period/periodShortLabels.ts +28 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodDraft.ts +1 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table/useRelationFilterLabels.ts +97 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table/useRowScrollAnchor.ts +106 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table/useTable.ts +9 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table/useTableColumns.ts +1 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/archivedCount.ts +31 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/filterEditorValue.ts +38 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/formatFilterValue.ts +33 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/scrollEdges.ts +31 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useCardFields.ts +33 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useMobileDisplay.ts +48 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useScrolledPastTop.ts +41 -18
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableFooter.ts +3 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLinkedVisit.ts +78 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLiveHold.ts +135 -22
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +56 -25
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +90 -35
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +86 -10
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/bulkActions.ts +14 -4
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/card.ts +127 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/compareValues.ts +13 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/expandedRows.ts +28 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/groupedRows.ts +114 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/linkedArrival.ts +308 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/locateRow.ts +106 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/pageArrival.ts +34 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/pinnedRows.ts +17 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/quickFilters.ts +41 -33
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/recordLink.ts +24 -8
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/tableCounts.ts +113 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/tableQuery.ts +27 -4
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/views.ts +22 -6
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/writeError.ts +21 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/addressSuggestions.ts +49 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/codeHighlight.ts +67 -29
- package/frontend-vue/layers/dms-ui/app/build/utils/eyebrow.ts +12 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/fieldOptionsI18n.ts +51 -11
- package/frontend-vue/layers/dms-ui/app/build/utils/htmlGrammar.ts +79 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/inputWidth.ts +53 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/numberAdjustment.ts +6 -3
- package/frontend-vue/layers/dms-ui/app/build/utils/relativeDate.ts +19 -5
- package/frontend-vue/layers/dms-ui/app/build/utils/scrollWithin.ts +99 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/shellParser.ts +1 -1
- package/frontend-vue/layers/dms-ui/app/build/utils/streamHighlight.ts +90 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +36 -14
- package/frontend-vue/layers/dms-ui/app/components/PasswordStrength.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/components/RecursiveComponent.vue +9 -1
- package/frontend-vue/layers/dms-ui/app/components/blocks/LiveToggleBlock.vue +80 -32
- package/frontend-vue/layers/dms-ui/app/components/blocks/PageToolbarBlock.vue +137 -0
- package/frontend-vue/layers/dms-ui/app/components/blocks/StatGroupBlock.vue +5 -0
- package/frontend-vue/layers/dms-ui/app/components/chart/ApexChartHost.vue +16 -2
- package/frontend-vue/layers/dms-ui/app/components/chart/Chart.vue +24 -5
- package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +41 -23
- package/frontend-vue/layers/dms-ui/app/components/code-snippet/CodeSnippet.vue +23 -12
- package/frontend-vue/layers/dms-ui/app/components/confirm/ConfirmModal.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/components/container-footer/ContainerFooter.vue +34 -0
- package/frontend-vue/layers/dms-ui/app/components/field-row/FieldRow.vue +25 -2
- package/frontend-vue/layers/dms-ui/app/components/flow-canvas/FlowCanvas.vue +2 -2
- package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +119 -36
- package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +53 -22
- package/frontend-vue/layers/dms-ui/app/components/form/components/DatePicker.vue +20 -1
- package/frontend-vue/layers/dms-ui/app/components/form/components/Display.vue +13 -1
- package/frontend-vue/layers/dms-ui/app/components/form/components/Image.vue +8 -3
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +53 -5
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputCode.vue +29 -5
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputNumber.vue +45 -8
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputPercentage.vue +28 -5
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputSecret.vue +5 -0
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputText.vue +19 -2
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputTime.vue +2 -1
- package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +226 -65
- package/frontend-vue/layers/dms-ui/app/components/form/components/Repeater.vue +80 -30
- package/frontend-vue/layers/dms-ui/app/components/form/components/RepeaterTable.vue +210 -0
- package/frontend-vue/layers/dms-ui/app/components/form/components/Select.vue +4 -2
- package/frontend-vue/layers/dms-ui/app/components/grid/GridRow.vue +11 -2
- package/frontend-vue/layers/dms-ui/app/components/key-value-list/KeyValueList.vue +79 -70
- package/frontend-vue/layers/dms-ui/app/components/kpi/KpiCard.vue +117 -21
- package/frontend-vue/layers/dms-ui/app/components/meter/Meter.vue +2 -1
- package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +113 -34
- package/frontend-vue/layers/dms-ui/app/components/save-bar/SaveBar.vue +14 -2
- package/frontend-vue/layers/dms-ui/app/components/section-header/Eyebrow.vue +6 -1
- package/frontend-vue/layers/dms-ui/app/components/segmented/Segmented.vue +13 -0
- package/frontend-vue/layers/dms-ui/app/components/stat-group/StatGroup.vue +52 -3
- package/frontend-vue/layers/dms-ui/app/components/tab/Tab.vue +2 -1
- package/frontend-vue/layers/dms-ui/app/components/table-view/CascaderPath.vue +74 -25
- package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanBoard.vue +48 -44
- package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanDisplay.vue +4 -1
- package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +426 -97
- package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +33 -5
- package/frontend-vue/layers/dms-ui/app/composables/chart/apexCategoryLabels.ts +68 -0
- package/frontend-vue/layers/dms-ui/app/composables/chart/apexDatetimeBuckets.ts +306 -0
- package/frontend-vue/layers/dms-ui/app/composables/chart/formatValue.ts +4 -3
- package/frontend-vue/layers/dms-ui/app/composables/chart/heatmapColors.ts +93 -0
- package/frontend-vue/layers/dms-ui/app/composables/chart/localizeSeries.ts +62 -0
- package/frontend-vue/layers/dms-ui/app/composables/chart/useApexChart.ts +116 -8
- package/frontend-vue/layers/dms-ui/app/composables/chart/useApexChart.types.ts +7 -0
- package/frontend-vue/layers/dms-ui/app/composables/containers/types.ts +84 -1
- package/frontend-vue/layers/dms-ui/app/composables/containers/useDrawer.ts +69 -8
- package/frontend-vue/layers/dms-ui/app/composables/form/types/field-saved.ts +11 -0
- package/frontend-vue/layers/dms-ui/app/composables/form/types/field.ts +16 -0
- package/frontend-vue/layers/dms-ui/app/composables/form/types/functions.ts +1 -0
- package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +189 -28
- package/frontend-vue/layers/dms-ui/app/composables/live/usePageLive.ts +29 -12
- package/frontend-vue/layers/dms-ui/app/composables/page-toolbar/usePageToolbarSize.ts +30 -0
- package/frontend-vue/layers/dms-ui/app/composables/table-view/kanban.ts +4 -2
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +22 -3
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +4 -0
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/display.ts +24 -1
- package/frontend-vue/layers/dms-ui/app/plugins/closing-layers.client.ts +10 -0
- package/frontend-vue/layers/dms-ui/app/plugins/live-pause-source.ts +15 -0
- package/frontend-vue/layers/dms-ui/app/plugins/register.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/types/input-number.ts +3 -2
- package/frontend-vue/layers/dms-ui/app/types/page-toolbar.ts +7 -0
- package/frontend-vue/layers/dms-ui/app/types/row-action.ts +0 -2
- package/frontend-vue/layers/dms-ui/app/types/row-navigation.ts +17 -0
- package/frontend-vue/layers/dms-ui/app/types/table/query.ts +2 -1
- package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +3 -1
- package/frontend-vue/layers/dms-ui/app/utils/cascader.ts +94 -3
- package/frontend-vue/layers/dms-ui/app/utils/componentTree.ts +24 -0
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +20 -5
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +20 -5
- package/frontend-vue/package.json +6 -2
- package/package.json +11 -10
- package/skills/dms/SKILL.md +1 -1
- package/skills/dms-auth/SKILL.md +5 -3
- package/skills/dms-best-practices/REFERENCE.md +2 -0
- package/skills/dms-best-practices/SKILL.md +4 -0
- package/skills/dms-module/SKILL.md +3 -2
- package/skills/dms-pages/REFERENCE.md +129 -16
- package/skills/dms-pages/SKILL.md +1 -1
- package/skills/dms-project/DEV-SERVERS.md +23 -1
- package/skills/dms-project/REFERENCE.md +1 -1
- package/skills/dms-project/SKILL.md +4 -1
- package/skills/dms-project/scripts/dms-dev.sh +205 -17
- package/dist/hooks/tenant-instances.d.ts +0 -1
- package/dist/hooks/tenant-instances.js +0 -13
- package/dist/hooks/tenant-instances.js.map +0 -1
- package/dist/routes/startup-gate.d.ts +0 -7
- package/dist/routes/startup-gate.js +0 -39
- package/dist/routes/startup-gate.js.map +0 -1
- package/frontend-vue/layers/dms-core/app/composables/period/periodRenderNow.ts +0 -42
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coordination between the tabs of one browser profile, for the long-lived
|
|
3
|
+
* streams a dashboard keeps open.
|
|
4
|
+
*
|
|
5
|
+
* Over HTTP/1.1 a browser opens at most six connections per host, and a
|
|
6
|
+
* streaming response holds one for as long as it lives: one stream per tab
|
|
7
|
+
* starves every other request once a few tabs are open. One tab is elected
|
|
8
|
+
* (a Web Lock, released by the browser when the tab goes, however it goes),
|
|
9
|
+
* opens the stream, and relays it to the others over a BroadcastChannel.
|
|
10
|
+
*
|
|
11
|
+
* Web Locks need a secure context (HTTPS, or localhost). Where either API is
|
|
12
|
+
* missing, every tab is its own leader on a bus of its own: the behaviour of a
|
|
13
|
+
* stream per tab, unchanged.
|
|
14
|
+
*
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const TAB_LOCK_SEPARATOR = ":tab:";
|
|
19
|
+
|
|
20
|
+
export type TabMessageHandler = (message: unknown) => void;
|
|
21
|
+
|
|
22
|
+
/** The browser APIs the coordination rides on; injected by the tests. */
|
|
23
|
+
export interface CrossTabEnvironment {
|
|
24
|
+
locks?: LockManager;
|
|
25
|
+
createChannel?: (name: string) => BroadcastChannel;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface TabBus {
|
|
29
|
+
/** Sends to every tab on the bus, this one included. */
|
|
30
|
+
post(message: unknown): void;
|
|
31
|
+
listen(handler: TabMessageHandler): () => void;
|
|
32
|
+
close(): void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A lock this tab holds until it releases it or goes. */
|
|
36
|
+
export interface HeldLock {
|
|
37
|
+
/** Resolves once the lock is held: until then, nobody can wait on it. */
|
|
38
|
+
held: Promise<void>;
|
|
39
|
+
release(): void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** What the running browser offers. */
|
|
43
|
+
export function browserCrossTabEnvironment(): CrossTabEnvironment {
|
|
44
|
+
const locks = globalThis.navigator?.locks;
|
|
45
|
+
const canBroadcast = typeof globalThis.BroadcastChannel === "function";
|
|
46
|
+
if (!locks || !canBroadcast) return {};
|
|
47
|
+
return { locks, createChannel: (name) => new BroadcastChannel(name) };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Whether the tabs can share: without both APIs each tab stays on its own. */
|
|
51
|
+
export function canShareAcrossTabs(environment: CrossTabEnvironment): boolean {
|
|
52
|
+
return !!environment.locks && !!environment.createChannel;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function openTabBus(
|
|
56
|
+
name: string,
|
|
57
|
+
environment: CrossTabEnvironment,
|
|
58
|
+
): TabBus {
|
|
59
|
+
const listeners = new Set<TabMessageHandler>();
|
|
60
|
+
const deliver = (message: unknown) => {
|
|
61
|
+
for (const listener of [...listeners]) listener(message);
|
|
62
|
+
};
|
|
63
|
+
const channel = canShareAcrossTabs(environment)
|
|
64
|
+
? environment.createChannel!(name)
|
|
65
|
+
: undefined;
|
|
66
|
+
if (channel) channel.onmessage = (event) => deliver(event.data);
|
|
67
|
+
return {
|
|
68
|
+
// A BroadcastChannel never echoes to its sender: this tab's own
|
|
69
|
+
// listeners hear the message directly.
|
|
70
|
+
post: (message) => {
|
|
71
|
+
channel?.postMessage(message);
|
|
72
|
+
deliver(message);
|
|
73
|
+
},
|
|
74
|
+
listen: (handler) => {
|
|
75
|
+
listeners.add(handler);
|
|
76
|
+
return () => listeners.delete(handler);
|
|
77
|
+
},
|
|
78
|
+
close: () => {
|
|
79
|
+
listeners.clear();
|
|
80
|
+
channel?.close();
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Runs `lead` in one tab at a time per `name`. When the leading tab resigns
|
|
87
|
+
* or goes, the next waiting tab takes over and runs its own `lead`.
|
|
88
|
+
*
|
|
89
|
+
* @param lead Starts the leader's work; returns what stops it
|
|
90
|
+
* @returns Resigns: stops the work when leading, or stops waiting
|
|
91
|
+
*/
|
|
92
|
+
export function electLeader(
|
|
93
|
+
name: string,
|
|
94
|
+
environment: CrossTabEnvironment,
|
|
95
|
+
lead: () => () => void,
|
|
96
|
+
): () => void {
|
|
97
|
+
if (!canShareAcrossTabs(environment)) return lead();
|
|
98
|
+
const abort = new AbortController();
|
|
99
|
+
let isResigned = false;
|
|
100
|
+
let stepDown: (() => void) | undefined;
|
|
101
|
+
environment
|
|
102
|
+
.locks!.request(name, { signal: abort.signal }, () => {
|
|
103
|
+
// Granted while the resignation was on its way.
|
|
104
|
+
if (isResigned) return;
|
|
105
|
+
return new Promise<void>((resolve) => {
|
|
106
|
+
const stop = lead();
|
|
107
|
+
stepDown = () => {
|
|
108
|
+
stop();
|
|
109
|
+
resolve();
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
})
|
|
113
|
+
.catch(() => {});
|
|
114
|
+
return () => {
|
|
115
|
+
isResigned = true;
|
|
116
|
+
abort.abort();
|
|
117
|
+
stepDown?.();
|
|
118
|
+
stepDown = undefined;
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function tabLockName(name: string, tabId: string): string {
|
|
123
|
+
return `${name}${TAB_LOCK_SEPARATOR}${tabId}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Holds `name` until released; the browser releases it when the tab goes. */
|
|
127
|
+
export function holdLock(
|
|
128
|
+
name: string,
|
|
129
|
+
environment: CrossTabEnvironment,
|
|
130
|
+
): HeldLock {
|
|
131
|
+
if (!canShareAcrossTabs(environment)) {
|
|
132
|
+
return { held: Promise.resolve(), release: () => {} };
|
|
133
|
+
}
|
|
134
|
+
const abort = new AbortController();
|
|
135
|
+
let release: (() => void) | undefined;
|
|
136
|
+
let isReleased = false;
|
|
137
|
+
const held = new Promise<void>((resolveHeld) => {
|
|
138
|
+
environment
|
|
139
|
+
.locks!.request(name, { signal: abort.signal }, () => {
|
|
140
|
+
if (isReleased) return;
|
|
141
|
+
resolveHeld();
|
|
142
|
+
return new Promise<void>((resolve) => {
|
|
143
|
+
release = resolve;
|
|
144
|
+
});
|
|
145
|
+
})
|
|
146
|
+
.catch(() => {});
|
|
147
|
+
});
|
|
148
|
+
return {
|
|
149
|
+
held,
|
|
150
|
+
release: () => {
|
|
151
|
+
isReleased = true;
|
|
152
|
+
abort.abort();
|
|
153
|
+
release?.();
|
|
154
|
+
release = undefined;
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Calls `onReleased` once nobody holds `name` any more: when the tab holding
|
|
161
|
+
* it releases it or goes. Never called once `signal` aborts.
|
|
162
|
+
*/
|
|
163
|
+
export function whenReleased(
|
|
164
|
+
name: string,
|
|
165
|
+
environment: CrossTabEnvironment,
|
|
166
|
+
onReleased: () => void,
|
|
167
|
+
signal: AbortSignal,
|
|
168
|
+
): void {
|
|
169
|
+
if (!canShareAcrossTabs(environment)) return;
|
|
170
|
+
environment
|
|
171
|
+
.locks!.request(name, { signal }, () => {
|
|
172
|
+
if (!signal.aborted) onReleased();
|
|
173
|
+
})
|
|
174
|
+
.catch(() => {});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* One source for every tab of the browser: the elected tab opens it, and what
|
|
179
|
+
* it emits reaches each tab's `onMessage`, the leader's included.
|
|
180
|
+
*
|
|
181
|
+
* @param open Opens the source, handing it what relays a message; returns
|
|
182
|
+
* what closes it
|
|
183
|
+
* @returns Leaves: closes the source when leading, and stops listening
|
|
184
|
+
*/
|
|
185
|
+
export function shareAcrossTabs(
|
|
186
|
+
name: string,
|
|
187
|
+
open: (emit: (message: unknown) => void) => () => void,
|
|
188
|
+
onMessage: TabMessageHandler,
|
|
189
|
+
environment: CrossTabEnvironment = browserCrossTabEnvironment(),
|
|
190
|
+
): () => void {
|
|
191
|
+
const bus = openTabBus(name, environment);
|
|
192
|
+
const unlisten = bus.listen(onMessage);
|
|
193
|
+
const resign = electLeader(name, environment, () => open(bus.post));
|
|
194
|
+
return () => {
|
|
195
|
+
resign();
|
|
196
|
+
unlisten();
|
|
197
|
+
bus.close();
|
|
198
|
+
};
|
|
199
|
+
}
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isUnauthorizedStreamError,
|
|
3
|
+
openSseStream,
|
|
4
|
+
type SseStreamHandle,
|
|
5
|
+
} from "../../composables/realtime/useSseStream";
|
|
6
|
+
import {
|
|
7
|
+
browserCrossTabEnvironment,
|
|
8
|
+
type CrossTabEnvironment,
|
|
9
|
+
electLeader,
|
|
10
|
+
holdLock,
|
|
11
|
+
openTabBus,
|
|
12
|
+
type TabBus,
|
|
13
|
+
tabLockName,
|
|
14
|
+
whenReleased,
|
|
15
|
+
} from "./crossTab";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The realtime connection one browser shares between its tabs: the elected tab
|
|
19
|
+
* holds `GET /api/realtime/connection` and relays it, each tab opens its own
|
|
20
|
+
* session on it. See `crossTab.ts` for why.
|
|
21
|
+
*
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const HELLO_EVENT = "hello";
|
|
26
|
+
const SESSION_EVENT = "session";
|
|
27
|
+
const SESSION_HEADER = "x-realtime-session";
|
|
28
|
+
const CONNECTION_HEADER = "x-realtime-connection";
|
|
29
|
+
const RECONNECT_INITIAL_DELAY_MS = 500;
|
|
30
|
+
const RECONNECT_MAX_DELAY_MS = 30_000;
|
|
31
|
+
const RECONNECT_MULTIPLIER = 2;
|
|
32
|
+
|
|
33
|
+
interface ConnectionMessage {
|
|
34
|
+
kind: "connection";
|
|
35
|
+
/** `null` while the leader has no stream open. */
|
|
36
|
+
connectionId: string | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface FrameMessage {
|
|
40
|
+
kind: "frame";
|
|
41
|
+
connectionId: string;
|
|
42
|
+
/** Unset on a frame of the connection itself, which every tab receives. */
|
|
43
|
+
sessionId?: string;
|
|
44
|
+
event: string;
|
|
45
|
+
data: unknown;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** A tab joining asks the leader for the connection it holds. */
|
|
49
|
+
interface QueryMessage {
|
|
50
|
+
kind: "query";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A tab's session on the connection, for the leader to end once it goes. */
|
|
54
|
+
interface SessionMessage {
|
|
55
|
+
kind: "session";
|
|
56
|
+
connectionId: string;
|
|
57
|
+
sessionId: string;
|
|
58
|
+
tabId: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A tab whose session request the backend answered without knowing the
|
|
63
|
+
* connection asks the leader to open a new one.
|
|
64
|
+
*/
|
|
65
|
+
interface ReopenMessage {
|
|
66
|
+
kind: "reopen";
|
|
67
|
+
connectionId: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
type HubMessage =
|
|
71
|
+
| ConnectionMessage
|
|
72
|
+
| FrameMessage
|
|
73
|
+
| QueryMessage
|
|
74
|
+
| SessionMessage
|
|
75
|
+
| ReopenMessage;
|
|
76
|
+
|
|
77
|
+
export interface StreamRecovery {
|
|
78
|
+
refreshSession: () => Promise<boolean>;
|
|
79
|
+
redirectToAuth: () => Promise<void>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface SharedRealtimeConnectionOptions extends StreamRecovery {
|
|
83
|
+
/** Scopes the election and the relay: one connection per name. */
|
|
84
|
+
name: string;
|
|
85
|
+
streamUrl: string;
|
|
86
|
+
sessionCloseUrl: string;
|
|
87
|
+
/** The connection to open a session on; `undefined` while there is none. */
|
|
88
|
+
onConnection: (connectionId: string | undefined) => void;
|
|
89
|
+
/**
|
|
90
|
+
* A frame of one tab's session, or of the connection itself (the dev
|
|
91
|
+
* reload signal) when `sessionId` is unset.
|
|
92
|
+
*/
|
|
93
|
+
onFrame: (
|
|
94
|
+
sessionId: string | undefined,
|
|
95
|
+
eventName: string,
|
|
96
|
+
data: unknown,
|
|
97
|
+
) => void;
|
|
98
|
+
environment?: CrossTabEnvironment;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface SharedRealtimeConnection {
|
|
102
|
+
/** Hands this tab's session to the leader, which ends it once the tab goes. */
|
|
103
|
+
announceSession: (connectionId: string, sessionId: string) => void;
|
|
104
|
+
/**
|
|
105
|
+
* Asks the leader to replace `connectionId` with a new stream: the backend
|
|
106
|
+
* answered a session request on it as a connection it does not know.
|
|
107
|
+
*/
|
|
108
|
+
reopen: (connectionId: string) => void;
|
|
109
|
+
leave: () => void;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface Reconnector {
|
|
113
|
+
isCancelled(): boolean;
|
|
114
|
+
schedule(open: () => void): void;
|
|
115
|
+
resetDelay(): void;
|
|
116
|
+
cancel(): void;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const createReconnector = (): Reconnector => {
|
|
120
|
+
let isCancelled = false;
|
|
121
|
+
let delay = RECONNECT_INITIAL_DELAY_MS;
|
|
122
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
123
|
+
return {
|
|
124
|
+
isCancelled: () => isCancelled,
|
|
125
|
+
schedule: (open) => {
|
|
126
|
+
// A pending reconnect absorbs concurrent triggers: one attempt at a time.
|
|
127
|
+
if (isCancelled || timer) return;
|
|
128
|
+
timer = setTimeout(() => {
|
|
129
|
+
timer = undefined;
|
|
130
|
+
open();
|
|
131
|
+
}, delay);
|
|
132
|
+
delay = Math.min(delay * RECONNECT_MULTIPLIER, RECONNECT_MAX_DELAY_MS);
|
|
133
|
+
},
|
|
134
|
+
resetDelay: () => {
|
|
135
|
+
delay = RECONNECT_INITIAL_DELAY_MS;
|
|
136
|
+
},
|
|
137
|
+
cancel: () => {
|
|
138
|
+
isCancelled = true;
|
|
139
|
+
if (timer) clearTimeout(timer);
|
|
140
|
+
timer = undefined;
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/** Retries after a failure, refreshing the browser session first on a 401. */
|
|
146
|
+
export const recoverFromError = (
|
|
147
|
+
error: unknown,
|
|
148
|
+
retry: () => void,
|
|
149
|
+
recovery: StreamRecovery,
|
|
150
|
+
): void => {
|
|
151
|
+
if (!isUnauthorizedStreamError(error)) {
|
|
152
|
+
retry();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
void recovery.refreshSession().then((isRefreshed) => {
|
|
156
|
+
if (isRefreshed) {
|
|
157
|
+
retry();
|
|
158
|
+
} else {
|
|
159
|
+
void recovery.redirectToAuth();
|
|
160
|
+
}
|
|
161
|
+
}, retry);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
interface StreamCallbacks extends StreamRecovery {
|
|
165
|
+
onEvent: (eventName: string, data: unknown) => void;
|
|
166
|
+
onDisconnected: () => void;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
interface KeptStream {
|
|
170
|
+
/** Drops the open stream and opens a new one. */
|
|
171
|
+
reopen: () => void;
|
|
172
|
+
stop: () => void;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Keeps one stream open until cancelled. The server ends the stream when it
|
|
177
|
+
* stops (a dev reload included), so a clean EOF reconnects just like an error
|
|
178
|
+
* does: the stream has no business completion.
|
|
179
|
+
*/
|
|
180
|
+
const connectWithBackoff = (
|
|
181
|
+
url: string,
|
|
182
|
+
callbacks: StreamCallbacks,
|
|
183
|
+
): KeptStream => {
|
|
184
|
+
const reconnector = createReconnector();
|
|
185
|
+
let activeHandle: SseStreamHandle | undefined;
|
|
186
|
+
const open = () => {
|
|
187
|
+
if (reconnector.isCancelled()) return;
|
|
188
|
+
activeHandle?.close();
|
|
189
|
+
const handle: SseStreamHandle = openSseStream({
|
|
190
|
+
url,
|
|
191
|
+
onEvent: (eventName, data) => {
|
|
192
|
+
if (handle !== activeHandle) return;
|
|
193
|
+
reconnector.resetDelay();
|
|
194
|
+
callbacks.onEvent(eventName, data);
|
|
195
|
+
},
|
|
196
|
+
onError: (error) => {
|
|
197
|
+
if (!retire(handle)) return;
|
|
198
|
+
recoverFromError(error, reconnect, callbacks);
|
|
199
|
+
},
|
|
200
|
+
onClose: (reason) => {
|
|
201
|
+
if (reason !== "eof" || !retire(handle)) return;
|
|
202
|
+
reconnect();
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
activeHandle = handle;
|
|
206
|
+
};
|
|
207
|
+
const reconnect = () => reconnector.schedule(open);
|
|
208
|
+
const retire = (handle: SseStreamHandle): boolean => {
|
|
209
|
+
if (handle !== activeHandle || reconnector.isCancelled()) return false;
|
|
210
|
+
activeHandle = undefined;
|
|
211
|
+
callbacks.onDisconnected();
|
|
212
|
+
return true;
|
|
213
|
+
};
|
|
214
|
+
open();
|
|
215
|
+
return {
|
|
216
|
+
reopen: () => {
|
|
217
|
+
const handle = activeHandle;
|
|
218
|
+
if (!handle || !retire(handle)) return;
|
|
219
|
+
handle.close();
|
|
220
|
+
reconnect();
|
|
221
|
+
},
|
|
222
|
+
stop: () => {
|
|
223
|
+
reconnector.cancel();
|
|
224
|
+
activeHandle?.close();
|
|
225
|
+
activeHandle = undefined;
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const readConnectionId = (data: unknown): string | null => {
|
|
231
|
+
const connectionId = (data as { connectionId?: unknown } | null)
|
|
232
|
+
?.connectionId;
|
|
233
|
+
return typeof connectionId === "string" ? connectionId : null;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
interface SessionFrame {
|
|
237
|
+
sessionId?: unknown;
|
|
238
|
+
event?: unknown;
|
|
239
|
+
data?: unknown;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
interface LeaderContext {
|
|
243
|
+
options: SharedRealtimeConnectionOptions;
|
|
244
|
+
environment: CrossTabEnvironment;
|
|
245
|
+
bus: TabBus;
|
|
246
|
+
tabId: string;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const closeSession = (
|
|
250
|
+
url: string,
|
|
251
|
+
connectionId: string,
|
|
252
|
+
sessionId: string,
|
|
253
|
+
): void => {
|
|
254
|
+
void fetch(url, {
|
|
255
|
+
method: "POST",
|
|
256
|
+
headers: { [CONNECTION_HEADER]: connectionId, [SESSION_HEADER]: sessionId },
|
|
257
|
+
credentials: "include",
|
|
258
|
+
}).catch(() => {});
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* The elected tab's share: the stream, relayed on the bus, and the sessions
|
|
263
|
+
* of the other tabs, each ended once its tab is gone.
|
|
264
|
+
*/
|
|
265
|
+
const lead = ({ options, environment, bus, tabId }: LeaderContext) => {
|
|
266
|
+
const watchers = new AbortController();
|
|
267
|
+
const sessionsByTab = new Map<string, Set<string>>();
|
|
268
|
+
let connectionId: string | null = null;
|
|
269
|
+
|
|
270
|
+
const publish = (next: string | null) => {
|
|
271
|
+
connectionId = next;
|
|
272
|
+
bus.post({ kind: "connection", connectionId: next });
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const endSessionsOf = (otherTabId: string) => {
|
|
276
|
+
const sessionIds = sessionsByTab.get(otherTabId) ?? new Set<string>();
|
|
277
|
+
sessionsByTab.delete(otherTabId);
|
|
278
|
+
// The sessions of a connection that went ended with it.
|
|
279
|
+
if (!connectionId) return;
|
|
280
|
+
for (const sessionId of sessionIds) {
|
|
281
|
+
closeSession(options.sessionCloseUrl, connectionId, sessionId);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const watchSession = (message: SessionMessage) => {
|
|
286
|
+
// This tab's own sessions end with the stream it holds.
|
|
287
|
+
if (message.tabId === tabId || message.connectionId !== connectionId) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const known = sessionsByTab.get(message.tabId);
|
|
291
|
+
if (known) {
|
|
292
|
+
known.add(message.sessionId);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
sessionsByTab.set(message.tabId, new Set([message.sessionId]));
|
|
296
|
+
whenReleased(
|
|
297
|
+
tabLockName(options.name, message.tabId),
|
|
298
|
+
environment,
|
|
299
|
+
() => endSessionsOf(message.tabId),
|
|
300
|
+
watchers.signal,
|
|
301
|
+
);
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const unlisten = bus.listen((raw) => {
|
|
305
|
+
const message = raw as HubMessage;
|
|
306
|
+
if (message.kind === "query")
|
|
307
|
+
bus.post({ kind: "connection", connectionId });
|
|
308
|
+
if (message.kind === "session") watchSession(message);
|
|
309
|
+
if (message.kind === "reopen" && message.connectionId === connectionId) {
|
|
310
|
+
stream.reopen();
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// The tabs may still hold the connection of a leader that went: they drop it.
|
|
315
|
+
publish(null);
|
|
316
|
+
const stream = connectWithBackoff(options.streamUrl, {
|
|
317
|
+
onEvent: (eventName, data) => {
|
|
318
|
+
if (eventName === HELLO_EVENT) {
|
|
319
|
+
sessionsByTab.clear();
|
|
320
|
+
publish(readConnectionId(data));
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (!connectionId) return;
|
|
324
|
+
if (eventName !== SESSION_EVENT) {
|
|
325
|
+
bus.post({ kind: "frame", connectionId, event: eventName, data });
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const frame = data as SessionFrame | null;
|
|
329
|
+
if (typeof frame?.sessionId !== "string") return;
|
|
330
|
+
if (typeof frame.event !== "string") return;
|
|
331
|
+
bus.post({
|
|
332
|
+
kind: "frame",
|
|
333
|
+
connectionId,
|
|
334
|
+
sessionId: frame.sessionId,
|
|
335
|
+
event: frame.event,
|
|
336
|
+
data: frame.data,
|
|
337
|
+
});
|
|
338
|
+
},
|
|
339
|
+
onDisconnected: () => publish(null),
|
|
340
|
+
refreshSession: options.refreshSession,
|
|
341
|
+
redirectToAuth: options.redirectToAuth,
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
return () => {
|
|
345
|
+
watchers.abort();
|
|
346
|
+
unlisten();
|
|
347
|
+
stream.stop();
|
|
348
|
+
publish(null);
|
|
349
|
+
};
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const createTabId = (): string =>
|
|
353
|
+
globalThis.crypto?.randomUUID?.() ??
|
|
354
|
+
`${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Joins the browser's realtime connection for `options.name`, electing this
|
|
358
|
+
* tab to hold it when no other tab does.
|
|
359
|
+
*/
|
|
360
|
+
export function joinSharedRealtimeConnection(
|
|
361
|
+
options: SharedRealtimeConnectionOptions,
|
|
362
|
+
): SharedRealtimeConnection {
|
|
363
|
+
const environment = options.environment ?? browserCrossTabEnvironment();
|
|
364
|
+
const tabId = createTabId();
|
|
365
|
+
const bus = openTabBus(options.name, environment);
|
|
366
|
+
// Held before any session is announced: the leader waits on it to learn
|
|
367
|
+
// that this tab went.
|
|
368
|
+
const tabLock = holdLock(tabLockName(options.name, tabId), environment);
|
|
369
|
+
let connectionId: string | undefined;
|
|
370
|
+
|
|
371
|
+
const unlisten = bus.listen((raw) => {
|
|
372
|
+
const message = raw as HubMessage;
|
|
373
|
+
if (message.kind === "connection") {
|
|
374
|
+
const next = message.connectionId ?? undefined;
|
|
375
|
+
if (next === connectionId) return;
|
|
376
|
+
connectionId = next;
|
|
377
|
+
options.onConnection(next);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (message.kind === "frame" && message.connectionId === connectionId) {
|
|
381
|
+
options.onFrame(message.sessionId, message.event, message.data);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
const resign = electLeader(options.name, environment, () =>
|
|
386
|
+
lead({ options, environment, bus, tabId }),
|
|
387
|
+
);
|
|
388
|
+
bus.post({ kind: "query" });
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
announceSession: (sessionConnectionId, sessionId) => {
|
|
392
|
+
void tabLock.held.then(() =>
|
|
393
|
+
bus.post({
|
|
394
|
+
kind: "session",
|
|
395
|
+
connectionId: sessionConnectionId,
|
|
396
|
+
sessionId,
|
|
397
|
+
tabId,
|
|
398
|
+
}),
|
|
399
|
+
);
|
|
400
|
+
},
|
|
401
|
+
reopen: (reopenedConnectionId) =>
|
|
402
|
+
bus.post({ kind: "reopen", connectionId: reopenedConnectionId }),
|
|
403
|
+
leave: () => {
|
|
404
|
+
resign();
|
|
405
|
+
unlisten();
|
|
406
|
+
tabLock.release();
|
|
407
|
+
bus.close();
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
}
|