@ahoo-wang/wow-view-store 9.2.0-rc.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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.es.js","names":["WowViewStoreErrorCodes","REFERENCED_BY_SHARED_DASHBOARD","CODES","ErrorCodes","cause","wow","status","at","depth","message","found","UNFILLED","unfilled","ViewStoreError","code","failure","known","errorCode","toWowError","error","name","SHARED_OWNER_ID","SYSTEM_OWNER_ID","SYSTEM_TENANT_ID","SCOPE","PATHS","place","variables","view","SUMMARY_FIELDS","version","snapshot","aggregateId","state","body","preferences","order","defaultInstanceId","autoRun","lastTabs","a","b","tabs","x","y","LIST_LIMIT","REMEMBERED_WRITES","NOT_REPLAYED","REMEMBERED_SYSTEM_VIEWS","options","query","listQuery","filter","definitionId","LIST_ORDER","personal","shared","system","thrown","seen","summary","AUDIENCE_RANK","id","signal","input","context","requestId","made","write","result","revision","audience","answered","retry","answer","current","remembered","relocated","plan","expected","stored","replayed","read","places","index","response","strict","boards","titles","board","isSystemInstanceId","first","instance","NO_VIEW_STORE","singleQuery","method","url","request","path","ResultExtractors","CommandHeaders","CommandStage","asc","run","isViewStoreError","limit","key","value"],"sources":["../src/errors.ts","../src/paths.ts","../src/wire.ts","../src/wowViewStore.ts"],"sourcesContent":["/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n ErrorCodes,\n toWowError,\n type BindingError,\n type WowError,\n} from '@ahoo-wang/wow-client';\nimport {\n ViewStoreError,\n type ViewStoreErrorCode,\n} from '@ahoo-wang/wow-view-engine';\n\n/**\n * The view store's own error codes, beside Wow's (`ErrorCodes` of\n * `@ahoo-wang/wow-client`).\n *\n * Named apart from the engine's `ViewStoreErrorCode` (the port's six codes): these\n * are the server's `errorCode` strings, which a `ViewStoreError` carries as\n * `detail.code`. Mirrors `ViewStoreErrorCodes` in\n * `view-store/wow-view-store-api/src/main/kotlin/me/ahoo/wow/viewstore/api/ViewStoreErrorCodes.kt`.\n */\nexport const WowViewStoreErrorCodes = Object.freeze({\n /** A write that is not a valid view or preferences write (HTTP 400). */\n VIEW_INVALID: 'ViewInvalid',\n /** The request carries no `CoSec-App-Id` (HTTP 400). */\n VIEW_APP_REQUIRED: 'ViewAppRequired',\n /** A system view is read-only (HTTP 403). */\n SYSTEM_VIEW_READ_ONLY: 'SystemViewReadOnly',\n /** The path's tenant or owner is missing, blank or invisible (HTTP 400). */\n VIEW_SCOPE_REQUIRED: 'ViewScopeRequired',\n /** The view store's event streams are closed to HTTP queries (HTTP 403). */\n VIEW_EVENT_STREAM_CLOSED: 'ViewEventStreamClosed',\n} as const);\n\n/**\n * The binding error code a claim refusal names each shared dashboard with\n * (`View.REFERENCED_BY_SHARED_DASHBOARD`): the board's id as `name`, its\n * title as `msg`.\n */\nexport const REFERENCED_BY_SHARED_DASHBOARD = 'referenced-by-shared-dashboard';\n\n/** The port's error code of each error code the server answers with. */\nconst CODES: Readonly<Record<string, ViewStoreErrorCode>> = {\n [ErrorCodes.COMMAND_EXPECT_VERSION_CONFLICT]: 'CONFLICT',\n [ErrorCodes.EVENT_VERSION_CONFLICT]: 'CONFLICT',\n [ErrorCodes.SOURCING_VERSION_CONFLICT]: 'CONFLICT',\n [ErrorCodes.NOT_FOUND]: 'NOT_FOUND',\n [ErrorCodes.ILLEGAL_ACCESS_DELETED_AGGREGATE]: 'NOT_FOUND',\n [ErrorCodes.ILLEGAL_ACCESS_OWNER_AGGREGATE]: 'FORBIDDEN',\n [ErrorCodes.ILLEGAL_ACCESS_SPACE_AGGREGATE]: 'FORBIDDEN',\n [ErrorCodes.ILLEGAL_ACCESS_QUERY_SCOPE]: 'FORBIDDEN',\n [WowViewStoreErrorCodes.SYSTEM_VIEW_READ_ONLY]: 'FORBIDDEN',\n [WowViewStoreErrorCodes.VIEW_EVENT_STREAM_CLOSED]: 'FORBIDDEN',\n [WowViewStoreErrorCodes.VIEW_INVALID]: 'INVALID',\n [WowViewStoreErrorCodes.VIEW_APP_REQUIRED]: 'INVALID',\n [WowViewStoreErrorCodes.VIEW_SCOPE_REQUIRED]: 'INVALID',\n [ErrorCodes.BAD_REQUEST]: 'INVALID',\n [ErrorCodes.ILLEGAL_ARGUMENT]: 'INVALID',\n // The server's own state, not the request: what a retry may get past.\n [ErrorCodes.ILLEGAL_STATE]: 'UNAVAILABLE',\n [ErrorCodes.COMMAND_VALIDATION]: 'INVALID',\n [ErrorCodes.DUPLICATE_AGGREGATE_ID]: 'INVALID',\n [ErrorCodes.QUERY_SCHEMA_VALIDATION]: 'INVALID',\n [ErrorCodes.REQUEST_TIMEOUT]: 'UNAVAILABLE',\n [ErrorCodes.TOO_MANY_REQUESTS]: 'UNAVAILABLE',\n [ErrorCodes.INTERNAL_SERVER_ERROR]: 'UNAVAILABLE',\n [ErrorCodes.QUERY_SCHEMA_UNAVAILABLE]: 'UNAVAILABLE',\n [ErrorCodes.QUERY_SCHEMA_CONFLICT]: 'UNAVAILABLE',\n};\n\n/**\n * A request that did not succeed, as the store reads it: the server's error,\n * when the server answered with one, and the HTTP status, when a response\n * came back at all.\n */\nexport class Failure {\n constructor(\n /** What the request threw. */\n readonly cause: unknown,\n /** The server's error, read from the failed response. */\n readonly wow?: WowError,\n /** The status of the failed response; absent when none came back. */\n readonly status?: number,\n ) {}\n\n /** The server's error code, if it answered with one. */\n get errorCode(): string | undefined {\n return this.wow?.errorCode;\n }\n\n get bindingErrors(): readonly BindingError[] {\n return this.wow?.bindingErrors ?? [];\n }\n\n /**\n * The path variable the fetcher's interceptors never filled, when that is\n * why the request was never sent: the host's set-up is wrong, not the\n * network.\n */\n get unfilled(): string | undefined {\n if (this.wow || this.status !== undefined) return undefined;\n for (let at: unknown = this.cause, depth = 0; at && depth < 4; depth++) {\n const message = (at as { message?: unknown }).message;\n const found = typeof message === 'string' ? UNFILLED.exec(message) : null;\n if (found) return found[1];\n at = (at as { cause?: unknown }).cause;\n }\n return undefined;\n }\n\n /**\n * The port's code for it (see {@link portCodeOf}). A path variable the\n * interceptors never filled is `INVALID`, as the server's own\n * `ViewScopeRequired` is: the request is wrong as built and a retry sends\n * the same — not `UNAVAILABLE`, which would offer one.\n */\n get code(): ViewStoreErrorCode {\n if (this.unfilled !== undefined) return 'INVALID';\n return portCodeOf(this.errorCode, this.status);\n }\n\n /**\n * Whether the server (or something in front of it) answered at all: a\n * response came back. An `UNAVAILABLE` that was answered is the server's\n * own error — a 5xx, a timeout it reported — and not the network.\n */\n get reached(): boolean {\n return this.wow !== undefined || this.status !== undefined;\n }\n\n get message(): string {\n if (this.wow) return this.wow.message;\n const unfilled = this.unfilled;\n if (unfilled !== undefined)\n return `The fetcher's interceptors did not fill the path variable {${unfilled}}`;\n if (this.status !== undefined)\n return `The view store answered HTTP ${this.status}`;\n return `The view store could not be reached: ${reasonOf(this.cause)}`;\n }\n\n /**\n * The port's error, holding what it was read from: the request's own\n * failure as `cause`, the server's error code as `detail` (a host tells\n * `ViewAppRequired` from `ViewInvalid` by it), and for `UNAVAILABLE`\n * whether the server answered (`reachable`).\n */\n toStoreError(): ViewStoreError {\n return new ViewStoreError(this.code, this.message, this.held());\n }\n\n /** {@link toStoreError} with another code: the same failure, read further. */\n held(): {\n cause: unknown;\n detail?: { code: string };\n reachable?: true;\n } {\n const code = this.errorCode;\n return {\n cause: this.cause,\n ...(code === undefined ? {} : { detail: { code } }),\n ...(this.code === 'UNAVAILABLE' && this.reached\n ? { reachable: true as const }\n : {}),\n };\n }\n}\n\n/** The fetcher's refusal of a route whose path variable has no value. */\nconst UNFILLED = /^Missing required path parameter: (\\S+)/;\n\n/** Whether the request id of a write was used before. */\nexport function isDuplicateRequest(failure: Failure): boolean {\n return failure.errorCode === ErrorCodes.DUPLICATE_REQUEST_ID;\n}\n\n/**\n * The port's code for a server's answer, **by Wow's error code first**: the\n * code says what the server decided, where one HTTP status carries several\n * decisions (a `400` is a stale version on no server, but a bad title and a\n * missing application alike). Only an answer without a code the store knows\n * — a gateway's own page, a proxy, a code a later server adds — falls back to\n * the status, and no answer at all (a network failure, a timeout, an abort)\n * is `UNAVAILABLE`: the outcome is unknown, and a retry under the same\n * `requestId` is what the port expects.\n */\nexport function portCodeOf(\n errorCode: string | undefined,\n status: number | undefined,\n): ViewStoreErrorCode {\n const known = errorCode === undefined ? undefined : CODES[errorCode];\n if (known) return known;\n switch (status) {\n case 400:\n case 422:\n return 'INVALID';\n case 401:\n case 403:\n return 'FORBIDDEN';\n case 404:\n case 410:\n return 'NOT_FOUND';\n case 409:\n case 412:\n return 'CONFLICT';\n default:\n return 'UNAVAILABLE';\n }\n}\n\n/** What a failed request threw, read as a {@link Failure}. */\nexport async function failureOf(error: unknown): Promise<Failure> {\n const wow = await toWowError(error);\n const status = wow?.status ?? statusOf(error);\n return new Failure(error, wow, status);\n}\n\n/** The status of a fetcher error's response, when it carries one. */\nfunction statusOf(error: unknown): number | undefined {\n if (typeof error !== 'object' || error === null) return undefined;\n const response = (error as { exchange?: { response?: Response } }).exchange\n ?.response;\n return response?.status;\n}\n\n/** What a refusal said; a `DOMException` is an `Error` only in some realms. */\nfunction reasonOf(error: unknown): string {\n if (typeof error === 'object' && error !== null) {\n const { name, message } = error as { name?: unknown; message?: unknown };\n if (typeof message === 'string')\n return typeof name === 'string' ? `${name}: ${message}` : message;\n }\n return String(error);\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The reserved owner of shared views and shared preferences\n * (`ViewStoreService.SHARED_OWNER_ID` on the server). The parentheses keep it\n * apart from every user id, so a path whose owner segment is `(shared)` is the\n * shared audience's.\n *\n * A host nobody signs in to fills `{ownerId}` with it by default (its own\n * request interceptor), and then has shared views and shared preferences\n * only.\n */\nexport const SHARED_OWNER_ID = '(shared)';\n\n/**\n * The reserved owner of stored system views\n * (`ViewStoreService.SYSTEM_OWNER_ID` on the server).\n */\nexport const SYSTEM_OWNER_ID = '(system)';\n\n/**\n * The one tenant stored system views live under\n * (`ViewStoreService.SYSTEM_TENANT_ID`): the value of CoSec's platform\n * tenant, not the default tenant `(0)`. System views are global, so the\n * store writes them on `tenant/(platform)/owner/(system)` whatever the\n * caller's own tenant, and the security gateway decides who may, by that\n * path.\n */\nexport const SYSTEM_TENANT_ID = '(platform)';\n\n/**\n * Where a view lives on the server: the owner segment of its path. `personal`\n * leaves `{ownerId}` to the fetcher's interceptors (fetcher-cosec's resource\n * attribution fills it from the token's `sub`); `shared` names\n * {@link SHARED_OWNER_ID}, which the interceptors never replace; `system`\n * names both the tenant and the owner of stored system views,\n * {@link SYSTEM_TENANT_ID} and {@link SYSTEM_OWNER_ID}.\n */\nexport type Place = 'personal' | 'shared' | 'system';\n\n/** Every route starts here; the tenant and owner are path variables. */\nconst SCOPE = '/view-store/tenant/{tenantId}/owner/{ownerId}';\n\n/** The view store's routes, relative to the fetcher's base URL. */\nexport const PATHS = {\n /** `POST`: create (the server generates the id). */\n views: `${SCOPE}/view`,\n /** `DELETE`: Wow's delete of the aggregate. */\n view: `${SCOPE}/view/{id}`,\n save: `${SCOPE}/view/{id}/save`,\n rename: `${SCOPE}/view/{id}/rename`,\n /** Sent to the view's personal path; moves it to `(shared)`. */\n share: `${SCOPE}/view/{id}/share`,\n /** Sent to the caller's own path; moves a shared view to the caller. */\n claim: `${SCOPE}/view/{id}/claim`,\n single: `${SCOPE}/view/snapshot/single`,\n list: `${SCOPE}/view/snapshot/list`,\n /** The view as the write with this request id left it; `204` for a delete. */\n replay: `${SCOPE}/view/requests/{requestId}`,\n /**\n * Served under `(shared)` only: the configured system views of the\n * caller's tenant and the stored ones, global.\n */\n systemViews: `${SCOPE}/system-views`,\n systemView: `${SCOPE}/system-views/{id}`,\n preferences: `${SCOPE}/definitions/{definitionId}/preferences`,\n} as const;\n\n/**\n * The path variables of a request at `place`. The tenant is the\n * interceptors' but on the system path; the owner is theirs for a personal\n * path.\n */\nexport function pathAt(\n place: Place,\n variables: Record<string, string> = {},\n): Record<string, string> {\n switch (place) {\n case 'shared':\n return { ...variables, ownerId: SHARED_OWNER_ID };\n case 'system':\n return {\n ...variables,\n tenantId: SYSTEM_TENANT_ID,\n ownerId: SYSTEM_OWNER_ID,\n };\n default:\n return { ...variables };\n }\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * The server's shapes, as far as the store reads them, and their reading as\n * the port's. The server stores the engine's `ViewConfig` whole and never\n * reads its meaning, so a config comes back exactly as it was saved.\n */\n\nimport type {\n ViewAudience,\n ViewConfig,\n ViewInstance,\n ViewInstanceSummary,\n ViewKind,\n ViewPreferences,\n} from '@ahoo-wang/wow-view-engine';\n\n/** The state of a `view` aggregate (`ViewState`); tenant and owner are Wow's. */\nexport interface ViewStateBody {\n definitionId: string;\n title: string;\n /** Follows the owner: `(shared)` is `shared`, any user is `personal`. */\n audience: ViewAudience;\n config: ViewConfig;\n}\n\n/**\n * A view's snapshot as a snapshot query or the replay route answers it. A\n * list projects it down to the summary's fields, `state.config.kind` among\n * them.\n */\nexport interface ViewSnapshotBody {\n aggregateId: string;\n version: number;\n state: ViewStateBody;\n}\n\n/** A system view the server serves (`SystemView`); `scope` is always `system`. */\nexport interface SystemViewBody {\n id: string;\n definitionId: string;\n title: string;\n kind: ViewKind;\n /** A hash of its content, whatever its source. */\n revision: string;\n config: ViewConfig;\n /**\n * `configured` (read-only) or `stored` (a view of `tenant/(platform)/owner/(system)`,\n * written through the view routes); absent from a server before it,\n * which served configured views only.\n */\n source?: 'configured' | 'stored';\n /** A stored view's aggregate version, which its writes expect. */\n version?: number | null;\n}\n\n/**\n * Whether the server stores `view` (else it configures it). A stored one\n * carries the port's `stored: true` (D81), the views an `editSystem`\n * permission may write; configured and code system views carry none.\n */\nexport function isStored(view: SystemViewBody): boolean {\n return view.source === 'stored' && typeof view.version === 'number';\n}\n\n/** One owner's preferences in one definition (`ViewPreferencesView`). */\nexport interface PreferencesBody {\n definitionId: string;\n order?: string[] | null;\n defaultInstanceId?: string | null;\n autoRun?: boolean | null;\n lastTabs?: Record<string, string> | null;\n /** `0` for preferences never written. */\n version: number;\n}\n\n/** The part of a command's answer (`CommandResult`) the store reads. */\nexport interface CommandResultBody {\n aggregateId: string;\n aggregateVersion?: number | null;\n}\n\n/** The fields a list reads, and nothing of the config but its `kind`. */\nexport const SUMMARY_FIELDS = [\n 'aggregateId',\n 'version',\n 'state.definitionId',\n 'state.title',\n 'state.audience',\n 'state.config.kind',\n];\n\n/** A snapshot's revision: the aggregate's version, compared as a string. */\nexport function revisionOf(version: number): string {\n return String(version);\n}\n\nexport function toInstance(snapshot: ViewSnapshotBody): ViewInstance {\n const { aggregateId, version, state } = snapshot;\n return {\n id: aggregateId,\n definitionId: state.definitionId,\n title: state.title,\n scope: state.audience,\n revision: revisionOf(version),\n config: state.config,\n };\n}\n\nexport function toSummary(snapshot: ViewSnapshotBody): ViewInstanceSummary {\n const { aggregateId, version, state } = snapshot;\n return {\n id: aggregateId,\n definitionId: state.definitionId,\n title: state.title,\n scope: state.audience,\n kind: state.config.kind,\n revision: revisionOf(version),\n };\n}\n\nexport function systemInstance(view: SystemViewBody): ViewInstance {\n return {\n id: view.id,\n definitionId: view.definitionId,\n title: view.title,\n scope: 'system',\n revision: view.revision,\n config: view.config,\n ...(isStored(view) ? { stored: true as const } : {}),\n };\n}\n\nexport function systemSummary(view: SystemViewBody): ViewInstanceSummary {\n return {\n id: view.id,\n definitionId: view.definitionId,\n title: view.title,\n scope: 'system',\n kind: view.kind,\n revision: view.revision,\n ...(isStored(view) ? { stored: true as const } : {}),\n };\n}\n\n/**\n * The port's preferences. A member the server holds as `null` is one never\n * set: `defaultInstanceId` reads as `null`, and `autoRun` and `lastTabs` are\n * left out, as the port's own `emptyPreferences()` leaves them.\n */\nexport function toPreferences(body: PreferencesBody): ViewPreferences {\n return preferencesAt(\n {\n order: body.order ?? [],\n defaultInstanceId: body.defaultInstanceId ?? null,\n autoRun: body.autoRun ?? undefined,\n lastTabs: body.lastTabs ?? undefined,\n },\n body.version,\n );\n}\n\n/** What a preferences write sends: everything but the revision. */\nexport function preferencesInput(\n preferences: Omit<ViewPreferences, 'revision'>,\n): Omit<ViewPreferences, 'revision'> {\n const { order, defaultInstanceId, autoRun, lastTabs } = preferences;\n return {\n order: [...order],\n defaultInstanceId: defaultInstanceId ?? null,\n ...(autoRun === undefined ? {} : { autoRun }),\n ...(lastTabs === undefined ? {} : { lastTabs: { ...lastTabs } }),\n };\n}\n\n/** `preferences` as written at `version`, in the port's shape. */\nexport function preferencesAt(\n preferences: Omit<ViewPreferences, 'revision'>,\n version: number,\n): ViewPreferences {\n return { ...preferencesInput(preferences), revision: revisionOf(version) };\n}\n\n/** Whether two preferences say the same, whatever their revisions. */\nexport function samePreferences(\n a: Omit<ViewPreferences, 'revision'>,\n b: Omit<ViewPreferences, 'revision'>,\n): boolean {\n return canonical(a) === canonical(b);\n}\n\n/** One spelling of what preferences say, the tabs in key order. */\nfunction canonical(preferences: Omit<ViewPreferences, 'revision'>): string {\n const { order, defaultInstanceId, autoRun, lastTabs } =\n preferencesInput(preferences);\n const tabs =\n lastTabs &&\n Object.entries(lastTabs).sort(([x], [y]) => (x < y ? -1 : x > y ? 1 : 0));\n return JSON.stringify([\n order,\n defaultInstanceId,\n autoRun ?? null,\n tabs ?? null,\n ]);\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { ResultExtractors, type Fetcher } from '@ahoo-wang/fetcher';\nimport {\n asc,\n CommandHeaders,\n CommandStage,\n filter,\n listQuery,\n singleQuery,\n type BindingError,\n} from '@ahoo-wang/wow-client';\nimport {\n isSystemInstanceId,\n isViewStoreError,\n ViewStoreError,\n type ViewAudience,\n type ViewConfig,\n type ViewInstance,\n type ViewInstanceSummary,\n type ViewPermissions,\n type ViewPreferences,\n type ViewStore,\n type WriteContext,\n} from '@ahoo-wang/wow-view-engine';\nimport {\n failureOf,\n Failure,\n isDuplicateRequest,\n REFERENCED_BY_SHARED_DASHBOARD,\n} from './errors.js';\nimport { PATHS, pathAt, type Place } from './paths.js';\nimport {\n isStored,\n preferencesAt,\n preferencesInput,\n revisionOf,\n samePreferences,\n SUMMARY_FIELDS,\n systemInstance,\n systemSummary,\n toInstance,\n toPreferences,\n toSummary,\n type CommandResultBody,\n type PreferencesBody,\n type SystemViewBody,\n type ViewSnapshotBody,\n} from './wire.js';\n\n/** How a {@link WowViewStore} reaches the view store. */\nexport interface WowViewStoreOptions {\n /**\n * The fetcher every request goes through, on the base URL that serves\n * `/view-store/…` (the CoSec gateway in front of the view store server, or\n * the service that embeds the starter).\n *\n * Its interceptors carry who is asking; the store never does. They must\n * fill the path variables `{tenantId}` and, on a personal path,\n * `{ownerId}` where the store leaves them out — fetcher-cosec's\n * `ResourceAttributionRequestInterceptor` fills them from the token's\n * `tenantId` and `sub` — and send `CoSec-App-Id` (fetcher-cosec's\n * `CoSecRequestInterceptor`), and the space and the authorization with it.\n * A host nobody signs in to adds an interceptor of its own that fills the\n * same defaults (the owner `(shared)`, {@link SHARED_OWNER_ID}).\n */\n fetcher: Fetcher;\n /**\n * Which buttons are enabled for one definition's views. The server does\n * not authorize, the CoSec gateway does; a host answers this by the roles\n * it holds there — `changeAudience` by the role that may write\n * `owner/(shared)`, which claiming a view needs. Left out, everything is\n * allowed, as the port reads a store without `permissions`.\n */\n permissions?: (definitionId: string) => ViewPermissions;\n}\n\n/**\n * The largest list the server answers (its query budget); a definition with\n * more views of one audience lists the first this many.\n */\nconst LIST_LIMIT = 1000;\n\n/** How many preference writes the store remembers the outcome of. */\nconst REMEMBERED_WRITES = 256;\n\n/** What a stored system view's writes expect: its version at a revision. */\ninterface StoredAt {\n /** The content hash the engine holds as the view's revision. */\n revision: string;\n version: number;\n}\n\n/** One instance write, as sent from the place the view is at. */\ninterface InstanceWrite {\n method: 'POST' | 'PUT' | 'DELETE';\n url: string;\n /** The path it is sent to, and so replayed on. */\n sentTo: Place;\n body?: unknown;\n /** Where the view is once it lands; `null` for a delete. */\n landsAt: Place | null;\n}\n\n/** The replay route found nothing this write can be answered with. */\nconst NOT_REPLAYED = Symbol('not replayed');\n\n/**\n * The view engine's `ViewStore` over the Wow view store (`view-store/` in the\n * Wow repository): saved views and preferences as two Wow aggregates, served\n * under `/view-store/tenant/{tenantId}/owner/{ownerId}/…`.\n *\n * **The owner segment is the audience.** A personal view lives on the\n * caller's own path (`{ownerId}` filled by the fetcher's interceptors), a\n * shared one on `owner/(shared)`; the CoSec gateway decides who may use\n * which. The port names a view by id alone, so the store remembers where it\n * last saw each one (a list, a read, a write) and looks an unknown id up on\n * the personal path, the shared path and the server's system views, in that\n * order. Setting a view shared or personal moves it between the two paths,\n * id kept.\n *\n * **Writes** carry the port's `requestId` as `Command-Request-Id`, its\n * `revision` as `Command-Aggregate-Version`, and wait for the snapshot; the\n * answer is the view read back at the version the write left. A write the\n * server refuses as a stale version or a repeated request id is first looked\n * up by its request id (the replay route): a retry answers what the first\n * attempt wrote. Otherwise a stale version is `CONFLICT` carrying the view as\n * it is now, and a view that turns out to be gone is `NOT_FOUND`.\n *\n * **Errors** are read by Wow's error code onto the port's five codes, by the\n * HTTP status only for an answer without a code the store knows; a request\n * that got no answer is `UNAVAILABLE`, and a retry under the same\n * `requestId` is safe.\n *\n * **Creating is not idempotent on the server**, which generates the id. A\n * store remembers the request ids of its own creates (bounded), and a retry\n * of one first asks the replay route whether it landed; a retry sent by\n * another store — another tab, a reload — makes a second view.\n */\nexport class WowViewStore implements ViewStore {\n private readonly fetcher: Fetcher;\n /**\n * Where each view was last seen, by id. Kept after a delete, for its retry.\n * A `system` view is read through the server's system views (configured\n * and stored) and written, when stored, on the system path.\n */\n private readonly places = new Map<string, Place>();\n /**\n * The preferences have no replay route, so the store keeps what each of\n * its own writes answered, and which ones it sent: a retry answers the\n * first outcome, and a retry whose first answer was lost is recognised.\n */\n private readonly preferenceOutcomes = new Remembered<ViewPreferences>();\n private readonly preferenceAttempts = new Remembered<true>();\n /** The request ids of the creates this store sent, for their retries. */\n private readonly createAttempts = new Remembered<true>();\n /**\n * The system views as last read, by id: a stored one's version at its\n * revision (a hash of its content, while a write expects the version), or\n * `null` for a configured one, which is read-only.\n */\n private readonly storedVersions = new Remembered<StoredAt | null>(\n REMEMBERED_SYSTEM_VIEWS,\n );\n /** The host's {@link WowViewStoreOptions.permissions}, when it gave any. */\n readonly permissions?: (definitionId: string) => ViewPermissions;\n\n constructor(options: WowViewStoreOptions) {\n this.fetcher = options.fetcher;\n // Left undefined when the host declared none, which the port reads as\n // \"everything is allowed\".\n if (options.permissions) this.permissions = options.permissions;\n }\n\n /**\n * The caller's personal views, the shared views and the server's system\n * views of the definition: three requests, sent together, answered in the\n * port's order — system, shared, personal, each oldest first (the server\n * sorts each audience by the time its first event was written).\n *\n * A server with no view store at all (one released before it) answers\n * every route `404`, where a list on one that has it never does: that is\n * `UNSUPPORTED`, not a missing view.\n */\n list(\n definitionId: string,\n signal?: AbortSignal,\n ): Promise<ViewInstanceSummary[]> {\n return guard(async () => {\n const query = listQuery({\n filter: filter.eq('state.definitionId', definitionId),\n projection: { include: SUMMARY_FIELDS },\n sort: LIST_ORDER,\n limit: LIST_LIMIT,\n });\n const [personal, shared, system] = await Promise.all([\n this.json<ViewSnapshotBody[]>('POST', PATHS.list, 'personal', {\n body: query,\n signal,\n }),\n this.json<ViewSnapshotBody[]>('POST', PATHS.list, 'shared', {\n body: query,\n signal,\n }),\n this.json<SystemViewBody[]>('GET', PATHS.systemViews, 'shared', {\n query: { definitionId },\n signal,\n }),\n ]).catch((thrown: unknown) => {\n throw thrown instanceof Failure && thrown.code === 'NOT_FOUND'\n ? unsupported(thrown)\n : thrown;\n });\n for (const view of system) this.rememberSystem(view);\n const seen = new Map<string, ViewInstanceSummary>();\n for (const summary of [\n ...personal.map(toSummary),\n ...shared.map(toSummary),\n ...system.map(systemSummary),\n ]) {\n if (seen.has(summary.id)) continue;\n seen.set(summary.id, summary);\n this.places.set(summary.id, summary.scope);\n }\n // Sorted stably: within an audience, the server's order stands.\n return [...seen.values()].sort(\n (a, b) => AUDIENCE_RANK[a.scope] - AUDIENCE_RANK[b.scope],\n );\n });\n }\n\n /** View `id` wherever it is: the caller's, shared, or the server's system view. */\n get(id: string, signal?: AbortSignal): Promise<ViewInstance> {\n return guard(async () => (await this.find(id, signal)).instance);\n }\n\n /**\n * Posts the view to the path of its `scope`; the server generates the id.\n * A retry of a create this store sent asks the replay route on that path\n * first, and answers the view the first attempt made when it landed; the\n * server itself does not deduplicate, so a retry from another store makes\n * a second view.\n */\n create(\n input: Omit<ViewInstance, 'id' | 'revision'>,\n context: WriteContext,\n ): Promise<ViewInstance> {\n return guard(async () => {\n // A system view is created on the system path, global (the gateway\n // decides who may); configured and code system views stay read-only.\n const place: Place = input.scope;\n const { requestId } = context;\n if (this.createAttempts.has(requestId)) {\n const made = await this.probe(place, context, true);\n const snapshot =\n made?.status === 200\n ? ((await made.json()) as ViewSnapshotBody | null)\n : null;\n if (snapshot?.aggregateId) {\n this.places.set(snapshot.aggregateId, place);\n // A system view's revision is its content hash, which the\n // snapshot does not carry: it is read from the system views.\n return place === 'system'\n ? this.readAt('system', snapshot.aggregateId, context.signal)\n : toInstance(snapshot);\n }\n }\n this.createAttempts.set(requestId, true);\n const { definitionId, title, config } = input;\n const write: InstanceWrite = {\n method: 'POST',\n url: PATHS.views,\n sentTo: place,\n body: { definitionId, title, config },\n landsAt: place,\n };\n const result = await this.json<CommandResultBody>(\n write.method,\n write.url,\n place,\n {\n body: write.body,\n headers: writeHeaders(context),\n signal: context.signal,\n },\n ).catch(async (thrown: unknown) => {\n throw await this.unsupportedOr(thrown, context.signal);\n });\n const landed = await this.landed(\n result.aggregateId,\n write,\n result,\n context,\n );\n return landed!;\n });\n }\n\n /** Replaces the view's config, at the expected `revision`. */\n save(\n id: string,\n config: ViewConfig,\n revision: string,\n context: WriteContext,\n ): Promise<ViewInstance> {\n return this.write(id, revision, context, place => ({\n method: 'PUT',\n url: PATHS.save,\n sentTo: place,\n body: { config },\n landsAt: place,\n })) as Promise<ViewInstance>;\n }\n\n /** Renames the view (the server trims the title), at the expected `revision`. */\n rename(\n id: string,\n title: string,\n revision: string,\n context: WriteContext,\n ): Promise<ViewInstance> {\n return this.write(id, revision, context, place => ({\n method: 'PUT',\n url: PATHS.rename,\n sentTo: place,\n body: { title },\n landsAt: place,\n })) as Promise<ViewInstance>;\n }\n\n /**\n * 设为共享 sends `share` to the path the view is at; 设为个人 sends `claim`\n * to the caller's own path, which the gateway admits only with the role\n * that may write `owner/(shared)`. Either answers a view that already has\n * the audience as it is, revision unmoved.\n *\n * A shared view a shared dashboard shows stays shared: the server refuses\n * the claim and names the boards, and the refusal carries them by\n * **title** in `boards`, as they are stored — a title written as a key\n * stays one, and is said where the engine shows the refusal.\n */\n changeAudience(\n id: string,\n audience: ViewAudience,\n revision: string,\n context: WriteContext,\n ): Promise<ViewInstance> {\n return this.write(id, revision, context, place =>\n place === 'system'\n ? readOnly('A system view never moves audience')\n : audience === 'shared'\n ? {\n method: 'PUT',\n url: PATHS.share,\n sentTo: place,\n body: {},\n landsAt: 'shared',\n }\n : {\n method: 'PUT',\n url: PATHS.claim,\n sentTo: 'personal',\n landsAt: 'personal',\n },\n ) as Promise<ViewInstance>;\n }\n\n /** Deletes the view, at the expected `revision`; a board showing it keeps a broken panel. */\n async delete(\n id: string,\n revision: string,\n context: WriteContext,\n ): Promise<void> {\n await this.write(id, revision, context, place => ({\n method: 'DELETE',\n url: PATHS.view,\n sentTo: place,\n body: {},\n landsAt: null,\n }));\n }\n\n /** The caller's own, or `(shared)`'s where the host fills that owner. */\n getPreferences(\n definitionId: string,\n signal?: AbortSignal,\n ): Promise<ViewPreferences> {\n return guard(async () =>\n toPreferences(\n await this.json<PreferencesBody>('GET', PATHS.preferences, 'personal', {\n path: { definitionId },\n signal,\n }).catch(async (thrown: unknown) => {\n throw await this.unsupportedOr(thrown, signal);\n }),\n ),\n );\n }\n\n /**\n * Writes the preferences at the expected `revision` (`'0'` for ones never\n * written). The server has no replay route for them, so a retry answers\n * what this store's first attempt answered, or — its answer lost — what is\n * stored when that is what it wrote.\n */\n setPreferences(\n definitionId: string,\n preferences: ViewPreferences,\n context: WriteContext,\n ): Promise<ViewPreferences> {\n return guard(async () => {\n const { requestId, signal } = context;\n const answered = this.preferenceOutcomes.get(requestId);\n if (answered) return structuredClone(answered);\n const retry = this.preferenceAttempts.has(requestId);\n this.preferenceAttempts.set(requestId, true);\n let answer: ViewPreferences;\n try {\n const result = await this.json<CommandResultBody>(\n 'PUT',\n PATHS.preferences,\n 'personal',\n {\n path: { definitionId },\n body: preferencesInput(preferences),\n headers: writeHeaders(context, preferences.revision),\n signal,\n },\n );\n answer =\n typeof result.aggregateVersion === 'number'\n ? preferencesAt(preferences, result.aggregateVersion)\n : await this.getPreferences(definitionId, signal);\n } catch (thrown) {\n if (\n !(thrown instanceof Failure) ||\n (thrown.code !== 'CONFLICT' && !isDuplicateRequest(thrown))\n )\n throw await this.unsupportedOr(thrown, signal);\n const current = await this.getPreferences(definitionId, signal);\n // A retry the server refuses because its first attempt landed: what\n // is stored says what it wrote, unless another writer moved it on.\n if (\n (retry || isDuplicateRequest(thrown)) &&\n samePreferences(current, preferences)\n )\n answer = current;\n else\n throw new ViewStoreError('CONFLICT', thrown.message, {\n ...thrown.held(),\n preferences: current,\n });\n }\n this.preferenceOutcomes.set(requestId, answer);\n return structuredClone(answer);\n });\n }\n\n /**\n * One instance write: to the place the view is at, then the answer read\n * back. A view remembered at a place it has since left (another tab shared\n * it) is looked up again and the write sent once more to where it is.\n */\n private write(\n id: string,\n revision: string,\n context: WriteContext,\n plan: (place: Place) => InstanceWrite,\n ): Promise<ViewInstance | undefined> {\n return guard(async () => {\n const { signal } = context;\n const remembered = this.places.get(id);\n let place = remembered ?? (await this.find(id, signal)).place;\n let relocated = remembered === undefined;\n for (;;) {\n const write = plan(place);\n let expected = revision;\n if (place === 'system') {\n const stored = await this.storedVersion(id, revision, write, context);\n if ('answer' in stored) return stored.answer;\n expected = stored.version;\n }\n let result: CommandResultBody;\n try {\n result = await this.json<CommandResultBody>(\n write.method,\n write.url,\n write.sentTo,\n {\n path: { id },\n body: write.body,\n headers: writeHeaders(context, expected),\n signal,\n },\n );\n } catch (thrown) {\n if (!(thrown instanceof Failure)) throw thrown;\n const replayed = await this.replayed(id, write, context, thrown);\n if (replayed !== NOT_REPLAYED) return replayed;\n if (\n !relocated &&\n (thrown.code === 'NOT_FOUND' || thrown.code === 'FORBIDDEN')\n ) {\n relocated = true;\n const found = (await this.find(id, signal, null)).place;\n // Only where the write would go elsewhere: a claim goes to the\n // caller's own path wherever the view is.\n if (found !== place && plan(found).sentTo !== write.sentTo) {\n place = found;\n continue;\n }\n }\n if (thrown.code === 'CONFLICT' || isDuplicateRequest(thrown))\n throw await this.conflict(id, thrown, signal);\n throw await this.refusal(thrown, signal);\n }\n // Landed: what goes wrong reading it back is never a reason to send\n // it again.\n return this.landed(id, write, result, context);\n }\n });\n }\n\n /** A landed write's answer: the view read back at the version it left. */\n private async landed(\n id: string,\n write: InstanceWrite,\n result: CommandResultBody,\n context: WriteContext,\n ): Promise<ViewInstance | undefined> {\n if (write.landsAt === null) return undefined;\n this.places.set(id, write.landsAt);\n const version = result.aggregateVersion;\n let read: ViewInstance | undefined;\n let failure: unknown;\n try {\n read = await this.readAt(write.landsAt, id, context.signal);\n if (typeof version !== 'number' || read.revision === revisionOf(version))\n return read;\n // A system view's revision is its content hash: the version it was\n // read at says whether it is this write's.\n if (write.landsAt === 'system') {\n if (this.storedVersions.get(id)?.version === version) return read;\n }\n } catch (thrown) {\n failure = thrown;\n }\n // Another writer moved it on between the write and the read: the replay\n // route answers it as this write left it. The write landed, so a probe\n // that fails is no reason to report it otherwise: what was read stands.\n const replayed = await this.replay(id, write, context, false);\n if (replayed !== NOT_REPLAYED && replayed !== undefined) return replayed;\n if (read) return read;\n throw failure;\n }\n\n /**\n * The answer of a write the server refused as a stale version, a repeated\n * request id or a missing view, when the refusal is that of a retry: the\n * replay route finds the first attempt by its request id.\n */\n private async replayed(\n id: string,\n write: InstanceWrite,\n context: WriteContext,\n failure: Failure,\n ): Promise<ViewInstance | undefined | typeof NOT_REPLAYED> {\n const retryable =\n failure.code === 'CONFLICT' ||\n failure.code === 'NOT_FOUND' ||\n isDuplicateRequest(failure);\n return retryable ? this.replay(id, write, context, true) : NOT_REPLAYED;\n }\n\n /**\n * What the write with the context's request id left of view `id`, from the\n * replay route: the view at that version, or `undefined` for a delete.\n *\n * The route finds a write only on the path of the owner who wrote it, and\n * a retry may be sent elsewhere than its first attempt — a share goes to\n * the personal path the view has left by the time it is retried — so the\n * path this attempt went to is asked first and the other one next.\n *\n * A probe that fails other than as \"not found\" is no answer on its path.\n * With `strict` — a refused write, whose own path is asked first — that\n * path's failure is passed on: whether the first attempt landed is\n * unknown, and a retry under the same request id is safe. The other path\n * (a caller without the shared role is refused there), and every probe of\n * a write that landed, count it as not replayed, and the caller keeps what\n * it knows: the refusal, or the view it read back.\n */\n private async replay(\n id: string,\n write: InstanceWrite,\n context: WriteContext,\n strict: boolean,\n ): Promise<ViewInstance | undefined | typeof NOT_REPLAYED> {\n const places: Place[] =\n write.sentTo === 'system'\n ? ['system']\n : write.sentTo === 'personal'\n ? ['personal', 'shared']\n : ['shared', 'personal'];\n for (const [index, place] of places.entries()) {\n const response = await this.probe(place, context, strict && index === 0);\n if (!response) continue;\n if (response.status === 204)\n return write.landsAt === null ? undefined : NOT_REPLAYED;\n const snapshot = (await response.json()) as ViewSnapshotBody;\n if (write.landsAt === null || snapshot?.aggregateId !== id)\n return NOT_REPLAYED;\n // The replay route answers a snapshot, whose revision would be its\n // version; a system view's is its content hash, which only the server\n // computes: the view is answered as it is now.\n if (write.landsAt === 'system')\n return this.readAt('system', id, context.signal);\n return toInstance(snapshot);\n }\n return NOT_REPLAYED;\n }\n\n /**\n * The replay route at `place` for the context's request id; `undefined`\n * when it knows none, or — not `strict` — when it could not be asked.\n */\n private async probe(\n place: Place,\n context: WriteContext,\n strict: boolean,\n ): Promise<Response | undefined> {\n try {\n return await this.send('GET', PATHS.replay, place, {\n path: { requestId: context.requestId },\n signal: context.signal,\n });\n } catch (thrown) {\n if (thrown instanceof Failure && (thrown.code === 'NOT_FOUND' || !strict))\n return undefined;\n throw thrown;\n }\n }\n\n /** A stale write's refusal, carrying the view as it is now. */\n private async conflict(\n id: string,\n failure: Failure,\n signal: AbortSignal | undefined,\n ): Promise<ViewStoreError> {\n const { instance } = await this.find(id, signal, null);\n return new ViewStoreError('CONFLICT', failure.message, {\n ...failure.held(),\n instance,\n });\n }\n\n /**\n * The port's refusal of a write. A claim the server refuses because shared\n * dashboards show the view carries those boards' titles (`boards`), which\n * the server gives beside each board's id (a board it gives no title for\n * is read for one); the engine says the refusal around them.\n */\n private async refusal(\n failure: Failure,\n signal: AbortSignal | undefined,\n ): Promise<ViewStoreError> {\n const boards = failure.bindingErrors.filter(\n error => error.code === REFERENCED_BY_SHARED_DASHBOARD,\n );\n if (failure.code !== 'INVALID' || boards.length === 0)\n return failure.toStoreError();\n const titles = await Promise.all(\n boards.map(board => this.boardTitle(board, signal)),\n );\n return new ViewStoreError('INVALID', failure.message, {\n ...failure.held(),\n boards: titles,\n });\n }\n\n private async boardTitle(\n board: BindingError,\n signal: AbortSignal | undefined,\n ): Promise<string> {\n if (typeof board.msg === 'string' && board.msg.trim() !== '')\n return board.msg;\n try {\n return (await this.readAt('shared', board.name, signal)).title;\n } catch {\n return board.name;\n }\n }\n\n /**\n * View `id` and where it is: first where it was last seen (`first`, `null`\n * to ignore that), then the personal path, the shared path and the\n * server's system views.\n */\n private async find(\n id: string,\n signal: AbortSignal | undefined,\n first: Place | null | undefined = this.places.get(id),\n ): Promise<{ place: Place; instance: ViewInstance }> {\n // Ids in `system:` are the views a host declares in code; the server\n // never issues or serves one.\n if (!isSystemInstanceId(id)) {\n const order: Place[] = ['personal', 'shared', 'system'];\n if (first)\n order.sort((a, b) => Number(b === first) - Number(a === first));\n for (const place of order) {\n try {\n const instance = await this.readAt(place, id, signal);\n this.places.set(id, place);\n return { place, instance };\n } catch (thrown) {\n if (thrown instanceof Failure && thrown.code === 'NOT_FOUND')\n continue;\n throw thrown;\n }\n }\n // Every place answered `404`: a view that is not there, or no view\n // store there at all (a server released before it) — the one\n // question the server's system views tell apart.\n if (!(await this.served(signal)))\n throw new ViewStoreError('UNSUPPORTED', NO_VIEW_STORE);\n }\n throw new ViewStoreError('NOT_FOUND', `No such view: ${id}`);\n }\n\n /**\n * Whether the server has a view store at all: its system views answer\n * on one that has (an empty list included), and `404` on one released\n * before it. Any other failure is no answer to that, and reads as served.\n */\n private async served(signal: AbortSignal | undefined): Promise<boolean> {\n try {\n await this.send('GET', PATHS.systemViews, 'shared', { signal });\n return true;\n } catch (thrown) {\n return !(thrown instanceof Failure && thrown.code === 'NOT_FOUND');\n }\n }\n\n /** A `404` from a server with no view store is `UNSUPPORTED`; else as it was. */\n private async unsupportedOr(\n thrown: unknown,\n signal: AbortSignal | undefined,\n ): Promise<unknown> {\n return thrown instanceof Failure &&\n thrown.code === 'NOT_FOUND' &&\n !(await this.served(signal))\n ? unsupported(thrown)\n : thrown;\n }\n\n /** Keeps what a stored system view's writes expect; forgets a configured one. */\n private rememberSystem(view: SystemViewBody): SystemViewBody {\n this.storedVersions.set(\n view.id,\n isStored(view)\n ? { revision: view.revision, version: view.version! }\n : null,\n );\n return view;\n }\n\n /**\n * The version a write of stored system view `id` at `revision` (its\n * content hash) expects: as last read, else read now. A configured system\n * view is read-only (`FORBIDDEN`; a code one is never found here). A\n * revision other than the view's is stale — unless this write is a retry\n * whose first attempt moved it on, which the replay route answers — and is\n * then `CONFLICT`, carrying the view as it is.\n */\n private async storedVersion(\n id: string,\n revision: string,\n write: InstanceWrite,\n context: WriteContext,\n ): Promise<{ version: string } | { answer: ViewInstance | undefined }> {\n let known = this.storedVersions.get(id);\n if (known === null) readOnly('Configured system views are read-only');\n if (known?.revision !== revision) {\n const instance = await this.readAt('system', id, context.signal);\n known = this.storedVersions.get(id);\n if (known && known.revision !== revision) {\n const replayed = await this.replay(id, write, context, false);\n if (replayed !== NOT_REPLAYED) return { answer: replayed };\n throw new ViewStoreError(\n 'CONFLICT',\n `System view ${id} has moved on from revision ${revision}`,\n { instance },\n );\n }\n }\n if (!known) readOnly('Configured system views are read-only');\n return { version: String(known.version) };\n }\n\n private async readAt(\n place: Place,\n id: string,\n signal: AbortSignal | undefined,\n ): Promise<ViewInstance> {\n if (place === 'system')\n return systemInstance(\n this.rememberSystem(\n await this.json<SystemViewBody>('GET', PATHS.systemView, 'shared', {\n path: { id },\n signal,\n }),\n ),\n );\n return toInstance(\n await this.json<ViewSnapshotBody>('POST', PATHS.single, place, {\n body: singleQuery({ filter: filter.id(id) }),\n signal,\n }),\n );\n }\n\n private async json<R>(\n method: string,\n url: string,\n place: Place,\n request: Request,\n ): Promise<R> {\n const response = await this.send(method, url, place, request);\n return (await response.json()) as R;\n }\n\n /**\n * The one place a request leaves the store, and so the one place what it\n * threw becomes a {@link Failure}.\n */\n private async send(\n method: string,\n url: string,\n place: Place,\n { path, query, body, headers, signal }: Request,\n ): Promise<Response> {\n try {\n return await this.fetcher.request<Response>(\n {\n url,\n method,\n urlParams: { path: pathAt(place, path), query },\n body: body as Record<string, unknown> | undefined,\n headers,\n signal,\n },\n { resultExtractor: ResultExtractors.Response },\n );\n } catch (error) {\n throw await failureOf(error);\n }\n }\n}\n\n/** One request's parts beside its method, route and place. */\ninterface Request {\n path?: Record<string, string>;\n query?: Record<string, string>;\n body?: unknown;\n headers?: Record<string, string>;\n signal?: AbortSignal;\n}\n\n/** The headers of a write: its request id, its expected version, the wait. */\nfunction writeHeaders(\n context: WriteContext,\n revision?: string,\n): Record<string, string> {\n return {\n [CommandHeaders.REQUEST_ID]: context.requestId,\n [CommandHeaders.WAIT_STAGE]: CommandStage.SNAPSHOT,\n ...(revision === undefined\n ? {}\n : { [CommandHeaders.AGGREGATE_VERSION]: revision }),\n };\n}\n\n/** What a server without a view store is told as. */\nconst NO_VIEW_STORE = 'This server has no view store';\n\nfunction unsupported(failure: Failure): ViewStoreError {\n return new ViewStoreError('UNSUPPORTED', NO_VIEW_STORE, failure.held());\n}\n\n/**\n * The port's list order, asked of the server per audience: oldest first, by\n * the time a view's first event was written, its id breaking a tie.\n */\nconst LIST_ORDER = [asc('firstEventTime'), asc('aggregateId')];\n\n/** System views first, then shared, then personal: the port's list order. */\nconst AUDIENCE_RANK: Readonly<Record<ViewInstanceSummary['scope'], number>> = {\n system: 0,\n shared: 1,\n personal: 2,\n};\n\n/** Refuses a write to a system view the server or the code declares. */\nfunction readOnly(message: string): never {\n throw new ViewStoreError('FORBIDDEN', message);\n}\n\n/**\n * Runs one of the port's methods, so that everything it rejects with is a\n * `ViewStoreError`: a failed request as its code says, anything else — a\n * body that did not parse — as `UNAVAILABLE`, the outcome unknown.\n */\nasync function guard<T>(run: () => Promise<T>): Promise<T> {\n try {\n return await run();\n } catch (thrown) {\n if (thrown instanceof Failure) throw thrown.toStoreError();\n if (isViewStoreError(thrown)) throw thrown;\n // A body that did not parse came back from something — a login wall's\n // page, a proxy's: answered, so not \"could not be reached\".\n throw new ViewStoreError(\n 'UNAVAILABLE',\n thrown instanceof Error ? thrown.message : String(thrown),\n {\n cause: thrown,\n ...(thrown instanceof SyntaxError ? { reachable: true as const } : {}),\n },\n );\n }\n}\n\n/**\n * How many system views the store remembers the version of; one it forgot\n * is read again before its write.\n */\nconst REMEMBERED_SYSTEM_VIEWS = 1024;\n\n/** A bounded memory by key: the oldest entry goes first. */\nclass Remembered<T> {\n private readonly entries = new Map<string, T>();\n\n constructor(private readonly limit = REMEMBERED_WRITES) {}\n\n get(key: string): T | undefined {\n return this.entries.get(key);\n }\n\n has(key: string): boolean {\n return this.entries.has(key);\n }\n\n set(key: string, value: T): void {\n this.entries.delete(key);\n this.entries.set(key, value);\n if (this.entries.size > this.limit)\n this.entries.delete(this.entries.keys().next().value!);\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * The reserved owner of shared views and shared preferences
3
+ * (`ViewStoreService.SHARED_OWNER_ID` on the server). The parentheses keep it
4
+ * apart from every user id, so a path whose owner segment is `(shared)` is the
5
+ * shared audience's.
6
+ *
7
+ * A host nobody signs in to fills `{ownerId}` with it by default (its own
8
+ * request interceptor), and then has shared views and shared preferences
9
+ * only.
10
+ */
11
+ export declare const SHARED_OWNER_ID = "(shared)";
12
+ /**
13
+ * The reserved owner of stored system views
14
+ * (`ViewStoreService.SYSTEM_OWNER_ID` on the server).
15
+ */
16
+ export declare const SYSTEM_OWNER_ID = "(system)";
17
+ /**
18
+ * The one tenant stored system views live under
19
+ * (`ViewStoreService.SYSTEM_TENANT_ID`): the value of CoSec's platform
20
+ * tenant, not the default tenant `(0)`. System views are global, so the
21
+ * store writes them on `tenant/(platform)/owner/(system)` whatever the
22
+ * caller's own tenant, and the security gateway decides who may, by that
23
+ * path.
24
+ */
25
+ export declare const SYSTEM_TENANT_ID = "(platform)";
26
+ /**
27
+ * Where a view lives on the server: the owner segment of its path. `personal`
28
+ * leaves `{ownerId}` to the fetcher's interceptors (fetcher-cosec's resource
29
+ * attribution fills it from the token's `sub`); `shared` names
30
+ * {@link SHARED_OWNER_ID}, which the interceptors never replace; `system`
31
+ * names both the tenant and the owner of stored system views,
32
+ * {@link SYSTEM_TENANT_ID} and {@link SYSTEM_OWNER_ID}.
33
+ */
34
+ export type Place = 'personal' | 'shared' | 'system';
35
+ /** The view store's routes, relative to the fetcher's base URL. */
36
+ export declare const PATHS: {
37
+ /** `POST`: create (the server generates the id). */
38
+ readonly views: "/view-store/tenant/{tenantId}/owner/{ownerId}/view";
39
+ /** `DELETE`: Wow's delete of the aggregate. */
40
+ readonly view: "/view-store/tenant/{tenantId}/owner/{ownerId}/view/{id}";
41
+ readonly save: "/view-store/tenant/{tenantId}/owner/{ownerId}/view/{id}/save";
42
+ readonly rename: "/view-store/tenant/{tenantId}/owner/{ownerId}/view/{id}/rename";
43
+ /** Sent to the view's personal path; moves it to `(shared)`. */
44
+ readonly share: "/view-store/tenant/{tenantId}/owner/{ownerId}/view/{id}/share";
45
+ /** Sent to the caller's own path; moves a shared view to the caller. */
46
+ readonly claim: "/view-store/tenant/{tenantId}/owner/{ownerId}/view/{id}/claim";
47
+ readonly single: "/view-store/tenant/{tenantId}/owner/{ownerId}/view/snapshot/single";
48
+ readonly list: "/view-store/tenant/{tenantId}/owner/{ownerId}/view/snapshot/list";
49
+ /** The view as the write with this request id left it; `204` for a delete. */
50
+ readonly replay: "/view-store/tenant/{tenantId}/owner/{ownerId}/view/requests/{requestId}";
51
+ /**
52
+ * Served under `(shared)` only: the configured system views of the
53
+ * caller's tenant and the stored ones, global.
54
+ */
55
+ readonly systemViews: "/view-store/tenant/{tenantId}/owner/{ownerId}/system-views";
56
+ readonly systemView: "/view-store/tenant/{tenantId}/owner/{ownerId}/system-views/{id}";
57
+ readonly preferences: "/view-store/tenant/{tenantId}/owner/{ownerId}/definitions/{definitionId}/preferences";
58
+ };
59
+ /**
60
+ * The path variables of a request at `place`. The tenant is the
61
+ * interceptors' but on the system path; the owner is theirs for a personal
62
+ * path.
63
+ */
64
+ export declare function pathAt(place: Place, variables?: Record<string, string>): Record<string, string>;
package/dist/wire.d.ts ADDED
@@ -0,0 +1,78 @@
1
+ import { ViewAudience, ViewConfig, ViewInstance, ViewInstanceSummary, ViewKind, ViewPreferences } from '@ahoo-wang/wow-view-engine';
2
+ /** The state of a `view` aggregate (`ViewState`); tenant and owner are Wow's. */
3
+ export interface ViewStateBody {
4
+ definitionId: string;
5
+ title: string;
6
+ /** Follows the owner: `(shared)` is `shared`, any user is `personal`. */
7
+ audience: ViewAudience;
8
+ config: ViewConfig;
9
+ }
10
+ /**
11
+ * A view's snapshot as a snapshot query or the replay route answers it. A
12
+ * list projects it down to the summary's fields, `state.config.kind` among
13
+ * them.
14
+ */
15
+ export interface ViewSnapshotBody {
16
+ aggregateId: string;
17
+ version: number;
18
+ state: ViewStateBody;
19
+ }
20
+ /** A system view the server serves (`SystemView`); `scope` is always `system`. */
21
+ export interface SystemViewBody {
22
+ id: string;
23
+ definitionId: string;
24
+ title: string;
25
+ kind: ViewKind;
26
+ /** A hash of its content, whatever its source. */
27
+ revision: string;
28
+ config: ViewConfig;
29
+ /**
30
+ * `configured` (read-only) or `stored` (a view of `tenant/(platform)/owner/(system)`,
31
+ * written through the view routes); absent from a server before it,
32
+ * which served configured views only.
33
+ */
34
+ source?: 'configured' | 'stored';
35
+ /** A stored view's aggregate version, which its writes expect. */
36
+ version?: number | null;
37
+ }
38
+ /**
39
+ * Whether the server stores `view` (else it configures it). A stored one
40
+ * carries the port's `stored: true` (D81), the views an `editSystem`
41
+ * permission may write; configured and code system views carry none.
42
+ */
43
+ export declare function isStored(view: SystemViewBody): boolean;
44
+ /** One owner's preferences in one definition (`ViewPreferencesView`). */
45
+ export interface PreferencesBody {
46
+ definitionId: string;
47
+ order?: string[] | null;
48
+ defaultInstanceId?: string | null;
49
+ autoRun?: boolean | null;
50
+ lastTabs?: Record<string, string> | null;
51
+ /** `0` for preferences never written. */
52
+ version: number;
53
+ }
54
+ /** The part of a command's answer (`CommandResult`) the store reads. */
55
+ export interface CommandResultBody {
56
+ aggregateId: string;
57
+ aggregateVersion?: number | null;
58
+ }
59
+ /** The fields a list reads, and nothing of the config but its `kind`. */
60
+ export declare const SUMMARY_FIELDS: string[];
61
+ /** A snapshot's revision: the aggregate's version, compared as a string. */
62
+ export declare function revisionOf(version: number): string;
63
+ export declare function toInstance(snapshot: ViewSnapshotBody): ViewInstance;
64
+ export declare function toSummary(snapshot: ViewSnapshotBody): ViewInstanceSummary;
65
+ export declare function systemInstance(view: SystemViewBody): ViewInstance;
66
+ export declare function systemSummary(view: SystemViewBody): ViewInstanceSummary;
67
+ /**
68
+ * The port's preferences. A member the server holds as `null` is one never
69
+ * set: `defaultInstanceId` reads as `null`, and `autoRun` and `lastTabs` are
70
+ * left out, as the port's own `emptyPreferences()` leaves them.
71
+ */
72
+ export declare function toPreferences(body: PreferencesBody): ViewPreferences;
73
+ /** What a preferences write sends: everything but the revision. */
74
+ export declare function preferencesInput(preferences: Omit<ViewPreferences, 'revision'>): Omit<ViewPreferences, 'revision'>;
75
+ /** `preferences` as written at `version`, in the port's shape. */
76
+ export declare function preferencesAt(preferences: Omit<ViewPreferences, 'revision'>, version: number): ViewPreferences;
77
+ /** Whether two preferences say the same, whatever their revisions. */
78
+ export declare function samePreferences(a: Omit<ViewPreferences, 'revision'>, b: Omit<ViewPreferences, 'revision'>): boolean;
@@ -0,0 +1,214 @@
1
+ import { Fetcher } from '@ahoo-wang/fetcher';
2
+ import { ViewAudience, ViewConfig, ViewInstance, ViewInstanceSummary, ViewPermissions, ViewPreferences, ViewStore, WriteContext } from '@ahoo-wang/wow-view-engine';
3
+ /** How a {@link WowViewStore} reaches the view store. */
4
+ export interface WowViewStoreOptions {
5
+ /**
6
+ * The fetcher every request goes through, on the base URL that serves
7
+ * `/view-store/…` (the CoSec gateway in front of the view store server, or
8
+ * the service that embeds the starter).
9
+ *
10
+ * Its interceptors carry who is asking; the store never does. They must
11
+ * fill the path variables `{tenantId}` and, on a personal path,
12
+ * `{ownerId}` where the store leaves them out — fetcher-cosec's
13
+ * `ResourceAttributionRequestInterceptor` fills them from the token's
14
+ * `tenantId` and `sub` — and send `CoSec-App-Id` (fetcher-cosec's
15
+ * `CoSecRequestInterceptor`), and the space and the authorization with it.
16
+ * A host nobody signs in to adds an interceptor of its own that fills the
17
+ * same defaults (the owner `(shared)`, {@link SHARED_OWNER_ID}).
18
+ */
19
+ fetcher: Fetcher;
20
+ /**
21
+ * Which buttons are enabled for one definition's views. The server does
22
+ * not authorize, the CoSec gateway does; a host answers this by the roles
23
+ * it holds there — `changeAudience` by the role that may write
24
+ * `owner/(shared)`, which claiming a view needs. Left out, everything is
25
+ * allowed, as the port reads a store without `permissions`.
26
+ */
27
+ permissions?: (definitionId: string) => ViewPermissions;
28
+ }
29
+ /**
30
+ * The view engine's `ViewStore` over the Wow view store (`view-store/` in the
31
+ * Wow repository): saved views and preferences as two Wow aggregates, served
32
+ * under `/view-store/tenant/{tenantId}/owner/{ownerId}/…`.
33
+ *
34
+ * **The owner segment is the audience.** A personal view lives on the
35
+ * caller's own path (`{ownerId}` filled by the fetcher's interceptors), a
36
+ * shared one on `owner/(shared)`; the CoSec gateway decides who may use
37
+ * which. The port names a view by id alone, so the store remembers where it
38
+ * last saw each one (a list, a read, a write) and looks an unknown id up on
39
+ * the personal path, the shared path and the server's system views, in that
40
+ * order. Setting a view shared or personal moves it between the two paths,
41
+ * id kept.
42
+ *
43
+ * **Writes** carry the port's `requestId` as `Command-Request-Id`, its
44
+ * `revision` as `Command-Aggregate-Version`, and wait for the snapshot; the
45
+ * answer is the view read back at the version the write left. A write the
46
+ * server refuses as a stale version or a repeated request id is first looked
47
+ * up by its request id (the replay route): a retry answers what the first
48
+ * attempt wrote. Otherwise a stale version is `CONFLICT` carrying the view as
49
+ * it is now, and a view that turns out to be gone is `NOT_FOUND`.
50
+ *
51
+ * **Errors** are read by Wow's error code onto the port's five codes, by the
52
+ * HTTP status only for an answer without a code the store knows; a request
53
+ * that got no answer is `UNAVAILABLE`, and a retry under the same
54
+ * `requestId` is safe.
55
+ *
56
+ * **Creating is not idempotent on the server**, which generates the id. A
57
+ * store remembers the request ids of its own creates (bounded), and a retry
58
+ * of one first asks the replay route whether it landed; a retry sent by
59
+ * another store — another tab, a reload — makes a second view.
60
+ */
61
+ export declare class WowViewStore implements ViewStore {
62
+ private readonly fetcher;
63
+ /**
64
+ * Where each view was last seen, by id. Kept after a delete, for its retry.
65
+ * A `system` view is read through the server's system views (configured
66
+ * and stored) and written, when stored, on the system path.
67
+ */
68
+ private readonly places;
69
+ /**
70
+ * The preferences have no replay route, so the store keeps what each of
71
+ * its own writes answered, and which ones it sent: a retry answers the
72
+ * first outcome, and a retry whose first answer was lost is recognised.
73
+ */
74
+ private readonly preferenceOutcomes;
75
+ private readonly preferenceAttempts;
76
+ /** The request ids of the creates this store sent, for their retries. */
77
+ private readonly createAttempts;
78
+ /**
79
+ * The system views as last read, by id: a stored one's version at its
80
+ * revision (a hash of its content, while a write expects the version), or
81
+ * `null` for a configured one, which is read-only.
82
+ */
83
+ private readonly storedVersions;
84
+ /** The host's {@link WowViewStoreOptions.permissions}, when it gave any. */
85
+ readonly permissions?: (definitionId: string) => ViewPermissions;
86
+ constructor(options: WowViewStoreOptions);
87
+ /**
88
+ * The caller's personal views, the shared views and the server's system
89
+ * views of the definition: three requests, sent together, answered in the
90
+ * port's order — system, shared, personal, each oldest first (the server
91
+ * sorts each audience by the time its first event was written).
92
+ *
93
+ * A server with no view store at all (one released before it) answers
94
+ * every route `404`, where a list on one that has it never does: that is
95
+ * `UNSUPPORTED`, not a missing view.
96
+ */
97
+ list(definitionId: string, signal?: AbortSignal): Promise<ViewInstanceSummary[]>;
98
+ /** View `id` wherever it is: the caller's, shared, or the server's system view. */
99
+ get(id: string, signal?: AbortSignal): Promise<ViewInstance>;
100
+ /**
101
+ * Posts the view to the path of its `scope`; the server generates the id.
102
+ * A retry of a create this store sent asks the replay route on that path
103
+ * first, and answers the view the first attempt made when it landed; the
104
+ * server itself does not deduplicate, so a retry from another store makes
105
+ * a second view.
106
+ */
107
+ create(input: Omit<ViewInstance, 'id' | 'revision'>, context: WriteContext): Promise<ViewInstance>;
108
+ /** Replaces the view's config, at the expected `revision`. */
109
+ save(id: string, config: ViewConfig, revision: string, context: WriteContext): Promise<ViewInstance>;
110
+ /** Renames the view (the server trims the title), at the expected `revision`. */
111
+ rename(id: string, title: string, revision: string, context: WriteContext): Promise<ViewInstance>;
112
+ /**
113
+ * 设为共享 sends `share` to the path the view is at; 设为个人 sends `claim`
114
+ * to the caller's own path, which the gateway admits only with the role
115
+ * that may write `owner/(shared)`. Either answers a view that already has
116
+ * the audience as it is, revision unmoved.
117
+ *
118
+ * A shared view a shared dashboard shows stays shared: the server refuses
119
+ * the claim and names the boards, and the refusal carries them by
120
+ * **title** in `boards`, as they are stored — a title written as a key
121
+ * stays one, and is said where the engine shows the refusal.
122
+ */
123
+ changeAudience(id: string, audience: ViewAudience, revision: string, context: WriteContext): Promise<ViewInstance>;
124
+ /** Deletes the view, at the expected `revision`; a board showing it keeps a broken panel. */
125
+ delete(id: string, revision: string, context: WriteContext): Promise<void>;
126
+ /** The caller's own, or `(shared)`'s where the host fills that owner. */
127
+ getPreferences(definitionId: string, signal?: AbortSignal): Promise<ViewPreferences>;
128
+ /**
129
+ * Writes the preferences at the expected `revision` (`'0'` for ones never
130
+ * written). The server has no replay route for them, so a retry answers
131
+ * what this store's first attempt answered, or — its answer lost — what is
132
+ * stored when that is what it wrote.
133
+ */
134
+ setPreferences(definitionId: string, preferences: ViewPreferences, context: WriteContext): Promise<ViewPreferences>;
135
+ /**
136
+ * One instance write: to the place the view is at, then the answer read
137
+ * back. A view remembered at a place it has since left (another tab shared
138
+ * it) is looked up again and the write sent once more to where it is.
139
+ */
140
+ private write;
141
+ /** A landed write's answer: the view read back at the version it left. */
142
+ private landed;
143
+ /**
144
+ * The answer of a write the server refused as a stale version, a repeated
145
+ * request id or a missing view, when the refusal is that of a retry: the
146
+ * replay route finds the first attempt by its request id.
147
+ */
148
+ private replayed;
149
+ /**
150
+ * What the write with the context's request id left of view `id`, from the
151
+ * replay route: the view at that version, or `undefined` for a delete.
152
+ *
153
+ * The route finds a write only on the path of the owner who wrote it, and
154
+ * a retry may be sent elsewhere than its first attempt — a share goes to
155
+ * the personal path the view has left by the time it is retried — so the
156
+ * path this attempt went to is asked first and the other one next.
157
+ *
158
+ * A probe that fails other than as "not found" is no answer on its path.
159
+ * With `strict` — a refused write, whose own path is asked first — that
160
+ * path's failure is passed on: whether the first attempt landed is
161
+ * unknown, and a retry under the same request id is safe. The other path
162
+ * (a caller without the shared role is refused there), and every probe of
163
+ * a write that landed, count it as not replayed, and the caller keeps what
164
+ * it knows: the refusal, or the view it read back.
165
+ */
166
+ private replay;
167
+ /**
168
+ * The replay route at `place` for the context's request id; `undefined`
169
+ * when it knows none, or — not `strict` — when it could not be asked.
170
+ */
171
+ private probe;
172
+ /** A stale write's refusal, carrying the view as it is now. */
173
+ private conflict;
174
+ /**
175
+ * The port's refusal of a write. A claim the server refuses because shared
176
+ * dashboards show the view carries those boards' titles (`boards`), which
177
+ * the server gives beside each board's id (a board it gives no title for
178
+ * is read for one); the engine says the refusal around them.
179
+ */
180
+ private refusal;
181
+ private boardTitle;
182
+ /**
183
+ * View `id` and where it is: first where it was last seen (`first`, `null`
184
+ * to ignore that), then the personal path, the shared path and the
185
+ * server's system views.
186
+ */
187
+ private find;
188
+ /**
189
+ * Whether the server has a view store at all: its system views answer
190
+ * on one that has (an empty list included), and `404` on one released
191
+ * before it. Any other failure is no answer to that, and reads as served.
192
+ */
193
+ private served;
194
+ /** A `404` from a server with no view store is `UNSUPPORTED`; else as it was. */
195
+ private unsupportedOr;
196
+ /** Keeps what a stored system view's writes expect; forgets a configured one. */
197
+ private rememberSystem;
198
+ /**
199
+ * The version a write of stored system view `id` at `revision` (its
200
+ * content hash) expects: as last read, else read now. A configured system
201
+ * view is read-only (`FORBIDDEN`; a code one is never found here). A
202
+ * revision other than the view's is stale — unless this write is a retry
203
+ * whose first attempt moved it on, which the replay route answers — and is
204
+ * then `CONFLICT`, carrying the view as it is.
205
+ */
206
+ private storedVersion;
207
+ private readAt;
208
+ private json;
209
+ /**
210
+ * The one place a request leaves the store, and so the one place what it
211
+ * threw becomes a {@link Failure}.
212
+ */
213
+ private send;
214
+ }
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "@ahoo-wang/wow-view-store",
3
+ "version": "9.2.0-rc.0",
4
+ "description": "The Wow view store as the view engine's ViewStore: saved views and preferences of @ahoo-wang/wow-view-engine on a Wow server (https://github.com/Ahoo-Wang/Wow).",
5
+ "keywords": [
6
+ "wow",
7
+ "wow-view-store",
8
+ "view-engine",
9
+ "view-store",
10
+ "cqrs",
11
+ "typescript"
12
+ ],
13
+ "author": "Ahoo-Wang",
14
+ "license": "Apache-2.0",
15
+ "homepage": "https://wow.ahoo.me/reference/typescript/wow-view-store/",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/Ahoo-Wang/Wow.git",
19
+ "directory": "typescript/wow-view-store"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/Ahoo-Wang/Wow/issues"
23
+ },
24
+ "type": "module",
25
+ "engines": {
26
+ "node": ">=22.12.0"
27
+ },
28
+ "module": "./dist/index.es.js",
29
+ "types": "./dist/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "import": "./dist/index.es.js",
34
+ "default": "./dist/index.es.js"
35
+ },
36
+ "./package.json": "./package.json"
37
+ },
38
+ "files": [
39
+ "dist",
40
+ "README.md",
41
+ "README.zh-CN.md"
42
+ ],
43
+ "sideEffects": false,
44
+ "peerDependencies": {
45
+ "@ahoo-wang/fetcher": "^5.1.5",
46
+ "@ahoo-wang/wow-client": "~9.2.0-rc.0",
47
+ "@ahoo-wang/wow-view-engine": "~9.2.0-rc.0"
48
+ },
49
+ "devDependencies": {
50
+ "@ahoo-wang/fetcher": "^5.1.5",
51
+ "@ahoo-wang/fetcher-decorator": "^5.1.5",
52
+ "@ahoo-wang/fetcher-eventstream": "^5.1.5",
53
+ "@eslint/js": "^10.0.1",
54
+ "@microsoft/api-extractor": "^7.59.3",
55
+ "@types/node": "^26.6.3",
56
+ "@vitest/coverage-v8": "4.1.11",
57
+ "eslint": "^10.11.0",
58
+ "globals": "^17.12.0",
59
+ "typescript": "~6.0.3",
60
+ "typescript-eslint": "^8.70.1",
61
+ "unplugin-dts": "1.1.1",
62
+ "vite": "8.3.1",
63
+ "vitest": "^4.1.11",
64
+ "@ahoo-wang/wow-client": "~9.2.0-rc.0",
65
+ "@ahoo-wang/wow-view-engine": "~9.2.0-rc.0"
66
+ },
67
+ "scripts": {
68
+ "build": "vite build && pnpm test:package",
69
+ "test:package": "node scripts/verify-package.mjs",
70
+ "test": "vitest run --coverage && pnpm test:type && pnpm test:api",
71
+ "test:no-coverage": "vitest run --coverage.enabled=false && pnpm test:type && pnpm test:api",
72
+ "test:type": "tsc --noEmit -p test/tsconfig.types.json",
73
+ "test:api": "node scripts/api-report.mjs",
74
+ "lint": "eslint . --fix",
75
+ "clean": "rm -rf dist"
76
+ }
77
+ }