@aiquants/daily-report 0.42.0 → 0.43.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 +146 -0
- package/README.md +184 -92
- package/dist/client.d.mts +15 -2
- package/dist/client.d.ts +15 -2
- package/dist/client.js +5 -5
- package/dist/client.mjs +5 -5
- package/dist/index.d.mts +3 -5
- package/dist/index.d.ts +3 -5
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/server.d.mts +4 -5
- package/dist/server.d.ts +4 -5
- package/dist/server.js +10 -10
- package/dist/server.mjs +10 -10
- package/dist/{comment-adapter-BTXFkMSz.d.ts → source-types-Bu-_yfcY.d.ts} +4 -2
- package/dist/{comment-adapter-wejvIziS.d.mts → source-types-NkQm-gNO.d.mts} +4 -2
- package/dist/{sse-schema-CUiQKk8K.d.mts → sse-schema-Br0b278w.d.mts} +165 -10
- package/dist/{sse-schema-CUiQKk8K.d.ts → sse-schema-Br0b278w.d.ts} +165 -10
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -595,7 +595,7 @@ its payload — with one strict parser each, after authentication and before any
|
|
|
595
595
|
shared draft-label test, beside the hub row) or true when its creation category is one of the names; a row whose flag reads as anything but 1 or 0
|
|
596
596
|
throws.
|
|
597
597
|
The client compares no name: the views open the viewer's own draft in the editor by `isDraft` alone, so a draft named by any of the names, or by a
|
|
598
|
-
variant the collation equates, behaves as one ([Client wiring](#client-wiring)). The SSE stream's key carries the wire version (`<streamKey>:
|
|
598
|
+
variant the collation equates, behaves as one ([Client wiring](#client-wiring)). The SSE stream's key carries the wire version (`<streamKey>:v4`; [Realtime architecture](#realtime-architecture)),
|
|
599
599
|
so an entry written before `isDraft` existed is never read.
|
|
600
600
|
- `resolveVisibleSourceTypes(request, { reason })` — **Required row-level authorization port.** Returns the `Hub.source_type` values this request may
|
|
601
601
|
view, or the explicit opt-out `{ unrestricted: "<reason>" }` of a deployment that does not restrict by source type. Applies uniformly to every server
|
|
@@ -639,7 +639,7 @@ its payload — with one strict parser each, after authentication and before any
|
|
|
639
639
|
|
|
640
640
|
The client has no host logger port: its four channels of the same list write to the console from `info` — `[DailyReportActionProvider]` (the action
|
|
641
641
|
provider: a failed action, the queue of SSE messages about uncached reports, a cache reload), `[DailyReportDetailLoader]` (the detail loading: a
|
|
642
|
-
stale update of a report's cache, a failed prefetch of a business date),
|
|
642
|
+
stale update of a report's cache, a failed prefetch of a business date, a failed refetch that left the shown report in place),
|
|
643
643
|
`[DailyReportIdsStream]` (the ids stream client: a revalidation that gave up, a rejected malformed line, which ends the attempt, a skipped line of
|
|
644
644
|
an unknown kind, a malformed stream anchor) and `[DailyReportSseConnection]` (the SSE connection hook).
|
|
645
645
|
Every line, the server's and the client's, carries exactly one channel prefix: no message spells a `[DailyReport…]` prefix of its own.
|
|
@@ -698,12 +698,15 @@ writes no line in the refusal record below (it is not a cross-site refusal). Eac
|
|
|
698
698
|
Each route's decisions — the name its refusals are recorded under (`api`, `action`, `sse` or `attachment`), the navigations it serves from another
|
|
699
699
|
origin, its answer to a data request and its 403 — come from one policy of that route, so they cannot be paired wrongly, and an endpoint added with a
|
|
700
700
|
streaming body is refused from the start.
|
|
701
|
+
The cross-origin rule itself is `crossOriginRefusal` of `@aiquants/request-isolation` (a dependency of this package, so every package that adopts it
|
|
702
|
+
refuses the same requests): a route's policy extends that package's `CrossOriginPolicy` (which navigations from another origin it serves), and the
|
|
703
|
+
rule answers why it refuses (`cross-origin-fetch` or `foreign-origin`, below), which the refusal record writes.
|
|
701
704
|
|
|
702
705
|
| Request | Answer |
|
|
703
706
|
| --- | --- |
|
|
704
707
|
| `Sec-Fetch-Site: same-origin` or `none` (a load the user started: the address bar, a bookmark) | Served: authentication and the route's own checks follow |
|
|
705
|
-
| Any other `Sec-Fetch-Site` — `same-site` (another origin of the same site, such as a sibling subdomain), `cross-site` or an unknown value | 403 before authentication, unless the request is a top-level document navigation that the route serves: method `GET`, `Sec-Fetch-Mode: navigate`, `Sec-Fetch-Dest` exactly `document`, and a route policy that accepts it. Only the attachment route accepts one, and only for an original (`inline` or `download`, read by the loader's own strict query parser): a link in another page opens or downloads the original. The API, SSE and thumbnail routes accept no navigation from another origin |
|
|
706
|
-
| No `Sec-Fetch-Site` | A safe method (`GET`, `HEAD`, `OPTIONS`) is served. An unsafe method is decided by `Origin`: no `Origin` is served (not a browser page), an `Origin` whose host is the host the request was addressed to (`new URL(request.url).host`, which the adapter builds from `Host`) is served, and `null`, an unparsable value, another host or another port is 403 |
|
|
708
|
+
| Any other `Sec-Fetch-Site` — `same-site` (another origin of the same site, such as a sibling subdomain), `cross-site` or an unknown value | 403 (`cross-origin-fetch`) before authentication, unless the request is a top-level document navigation that the route serves: method `GET`, `Sec-Fetch-Mode: navigate`, `Sec-Fetch-Dest` exactly `document`, and a route policy that accepts it. Only the attachment route accepts one, and only for an original (`inline` or `download`, read by the loader's own strict query parser): a link in another page opens or downloads the original. The API, SSE and thumbnail routes accept no navigation from another origin |
|
|
709
|
+
| No `Sec-Fetch-Site` | A safe method (`GET`, `HEAD`, `OPTIONS`) is served. An unsafe method is decided by `Origin`: no `Origin` is served (not a browser page), an `Origin` whose host is the host the request was addressed to (`new URL(request.url).host`, which the adapter builds from `Host`) is served, and `null`, an unparsable value, another host or another port is 403 (`foreign-origin`) |
|
|
707
710
|
|
|
708
711
|
- **Requests without Fetch Metadata**: a browser sends no `Sec-Fetch-*` to an origin that is not potentially trustworthy (plain HTTP other than
|
|
709
712
|
`localhost`), so such a request comes either from a client that is not a browser or from a page of such an origin. A browser always sends `Origin`
|
|
@@ -721,7 +724,9 @@ streaming body is refused from the start.
|
|
|
721
724
|
- **The refusal record**: each handler factory keeps one bounded record of its refusals (state per factory), written on the `[DailyReportIsolation]`
|
|
722
725
|
channel: to the configured `logger` with that prefix, or by default to the console from the `warn` level.
|
|
723
726
|
A refusal with no open window writes one warn line at once,
|
|
724
|
-
`cross_site_rejected route=<api|action|sse|attachment> site=<value> mode=<value> dest=<value> method=<method>`
|
|
727
|
+
`cross_site_rejected route=<api|action|sse|attachment> reason=<cross-origin-fetch|foreign-origin> site=<value> mode=<value> dest=<value> method=<method>`
|
|
728
|
+
(the reason says which half of the rule refused: `cross-origin-fetch` when the Fetch Metadata names another origin, `foreign-origin` when a request
|
|
729
|
+
without Fetch Metadata uses an unsafe method and an `Origin` that does not name the request's host; it carries nothing the request sent; `-` for an absent header; a value
|
|
725
730
|
outside the specification's shape — lower-case letters and hyphens, at most 32 characters — is written as the JSON string of its first 32
|
|
726
731
|
characters, so a sender cannot forge the line's fields), and opens a window of 60 s that every route of the factory shares; later refusals in the
|
|
727
732
|
window are only counted, per route.
|
|
@@ -784,8 +789,9 @@ reason is the audit trail, so `grep -rn "unrestricted:"` in the host lists every
|
|
|
784
789
|
`"Internal"`, the package's own source type for the reports it creates (`DAILY_REPORT_INTERNAL_SOURCE_TYPE` on the shared entry). They are *not* your
|
|
785
790
|
authorization resource keys; mapping a resource key (e.g. `report_legacy`) onto a source type (e.g. `legacy`) is the consuming app's job. Read the
|
|
786
791
|
canonical set at boot from `dailyReportServer.knownSourceTypes` (frozen: `Internal` first, then each adapter's in order) and assert your mapping
|
|
787
|
-
against it — an unknown token silently matches zero rows. Fold your tokens with `foldDailyReportSourceTypeToken` (shared entry
|
|
788
|
-
|
|
792
|
+
against it — an unknown token silently matches zero rows. Fold your tokens with `foldDailyReportSourceTypeToken` (the shared entry, and the client entry for route modules that ship
|
|
793
|
+
to the browser: the shared entry is not split, so importing the fold from it carries a second copy of shared modules into the route's chunk), the
|
|
794
|
+
fold the server applies to the tokens it reads, instead of a copy of it.
|
|
789
795
|
Two adapters whose source types are equal ignoring case (an exact duplicate included), or an adapter whose source type is `Internal` in any case,
|
|
790
796
|
stop `createDailyReportService` and `createDailyReportServer` with a `RangeError`
|
|
791
797
|
(`[daily-report] externalSources[1].sourceType must be a source type that differs, ignoring case, from externalSources[0].sourceType "legacy"; got "Legacy"`):
|
|
@@ -1617,8 +1623,8 @@ and without the retry the victim would answer 500 and lose what it wrote.
|
|
|
1617
1623
|
|
|
1618
1624
|
### Realtime architecture
|
|
1619
1625
|
|
|
1620
|
-
1. Mutation services publish to Redis Stream (`<streamKey>:
|
|
1621
|
-
embedded report, so a deployment that changes a required key of
|
|
1626
|
+
1. Mutation services publish to Redis Stream (`<streamKey>:v4`, `daily-report:sse-stream:v4` by default: the key carries the wire version of the
|
|
1627
|
+
stream's messages and their embedded report, so a deployment that changes a required key of them reads no entry an older version wrote, and the stream of the previous version
|
|
1622
1628
|
stops growing and can be deleted by hand; a host that reads or writes the stream itself, such as an integration test, builds the key with the
|
|
1623
1629
|
server entry's `buildDailyReportSseStreamKey(streamKey)` instead of restating the version) in sequence:
|
|
1624
1630
|
`invalidate -> epoch increment -> SSE publish`.
|
|
@@ -1627,20 +1633,27 @@ and without the retry the victim would answer 500 and lose what it wrote.
|
|
|
1627
1633
|
`DailyReportSseReaderPort` — `ready()` and `subscribe(onEntry, onError)`, with the contract under **`connected` frame** below — which is the type a
|
|
1628
1634
|
host's own reader implements: a plain object with the two methods is accepted, while the class type, which has private fields, would refuse one.
|
|
1629
1635
|
The reader reads each live entry once per process and hands every subscriber the same frozen value (`DailyReportSseStreamEntry`:
|
|
1630
|
-
`{ id, message }`): `message` is the entry's `data` field parsed as JSON and validated by `
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1636
|
+
`{ id, message }`): `message` is the entry's `data` field parsed as JSON and validated by the stream form `dailyReportSseStreamMessageSchema` (the
|
|
1637
|
+
only form that declares the server-only fields: the six types that may be addressed — status-update, comment-add, comment-delete, report-create,
|
|
1638
|
+
report-update and report-publish — carry the recipient's internal id `recipientRawUserId`, and the three that embed a report also the writer's,
|
|
1639
|
+
`actorRawUserId`) — the validated message (`parsed`) and the JSON value as published (`published`, which only the recipient filter reads) — or
|
|
1640
|
+
`null` when the field is absent, is not JSON or fails the schema (the last two logged once at `error`; such an entry is written to no connection —
|
|
1641
|
+
fail-closed — while the reader's own read position moves past it).
|
|
1642
|
+
Each connection only filters that value (recipient, source-type visibility, comment redaction, the actor) and never changes it. Every frame is
|
|
1643
|
+
built from the parsed message, never from the published value or text: the message without its server-only fields, typed as the delivered form,
|
|
1644
|
+
so a server-only field left in a frame fails the type check, and a key the stream schema does not know never reaches a viewer. A message that
|
|
1645
|
+
embeds no report has one frame for every viewer; one that embeds a report has at most four — with the viewer's `viewerIsActor`, its comments
|
|
1646
|
+
emptied for a viewer who may not read them — each built at most once per message and shared by every connection that writes it, so a live entry
|
|
1647
|
+
costs one parse in proportion to its size plus a filter per connection, not one parse per connection, and every frame is one line of JSON.
|
|
1638
1648
|
A host's own reader keeps the same contract: it reads an entry once — `readDailyReportSseStreamEntry(raw, logger)` (server entry) reads it as the
|
|
1639
1649
|
package's reader does — and hands the same value to every subscriber.
|
|
1640
1650
|
3. `sse.loader` is built on `@aiquants/sse/server` (`createSseResponse`, `terminalStreamResponse`, `startSseHeartbeat`, `readLastEventId`). Its
|
|
1641
1651
|
responses carry the SSE response headers of `@aiquants/sse`, the routes' security headers (**Security headers** in [The API route](#the-api-route))
|
|
1642
|
-
and the forwarded `Set-Cookie`, and no `Connection` or other hop-by-hop header (which HTTP/2 forbids). `recipientRawUserId`
|
|
1643
|
-
and
|
|
1652
|
+
and the forwarded `Set-Cookie`, and no `Connection` or other hop-by-hop header (which HTTP/2 forbids). `recipientRawUserId` and `actorRawUserId` are read
|
|
1653
|
+
server-side and never written to a viewer, to prevent internal ID leaks; each connection writes `viewerIsActor` instead (whether its viewer made
|
|
1654
|
+
the write), so the delivered form (`dailyReportSseMessageSchema`, the root entry's `DailyReportSseMessage`) declares `viewerIsActor` and no internal id
|
|
1655
|
+
on any type. The delivered schema does not refuse a key it does not know (it drops it), so a long-lived tab keeps accepting a newer server's
|
|
1656
|
+
messages that carry additive fields; what a viewer receives is decided by the frame builder above. Catch-up and live entries pass through the same filters (recipient, source-type
|
|
1644
1657
|
visibility, comment redaction).
|
|
1645
1658
|
4. On the client, `useDailyReportSseConnection` runs on `useReopeningEventSource` (`@aiquants/sse/react`): full-jitter backoff from 2 s to 30 s, the
|
|
1646
1659
|
failure count reset only after 60 s of healthy open (the 45 s stale window plus one heartbeat period), one native browser reconnect (which carries
|
|
@@ -1658,6 +1671,9 @@ and without the retry the victim would answer 500 and lose what it wrote.
|
|
|
1658
1671
|
A comment that arrives by SSE is the viewer's own exactly when its author id equals the viewer's (`userId`, from the deterministic `encodeUserId`
|
|
1659
1672
|
port), whichever tab or device posted it; a comment of another user with the same text as one being posted is never taken for it, and a posted
|
|
1660
1673
|
comment's temporary id becomes its real id only through its own action's answer.
|
|
1674
|
+
A report message (create, update, publish) is the viewer's own write exactly when it carries `viewerIsActor: true`: the client then caches the
|
|
1675
|
+
payload's read and star state as its own write's answer, so the writer's other tabs and devices see their own create as read; otherwise it merges the
|
|
1676
|
+
shared fields and keeps the viewer's own read and star state (false for a report it has not cached), so no write leaks its writer's state.
|
|
1661
1677
|
Each action holds its own lock on the report it changes (its release frees only that lock and only once), so an overlapping action, an SSE change
|
|
1662
1678
|
(which records the change time without locking) and the 1 s lock after a creation (which a later hold only extends) never end one another's locks.
|
|
1663
1679
|
|
|
@@ -2131,14 +2147,17 @@ and that a match found through a reading is underlined with dots.
|
|
|
2131
2147
|
While a search request runs or after it failed, the views keep the list they showed; an answer replaces the list in its order (a report the provider
|
|
2132
2148
|
marked removed is left out), and the header shows the hit count. While an answer shows, the rows read their details by id in batches (`reports?ids=`)
|
|
2133
2149
|
instead of by business date, and an answer goes into the per-report cache only, never into a business date's cached list.
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2150
|
+
Each view leads its rows' inputs: it keeps its rendered rows and 24–40 rows beyond each rendered edge subscribed to the details and the marks, in
|
|
2151
|
+
chunks of 8 rows (32 on each side after a jump; **Highlights** below), so the rows a held key mounts find their details already loaded.
|
|
2152
|
+
An id joins the waiting ids at the first animation frame after rows stop entering and leaving, so a fast scroll requests nothing for the rows it
|
|
2153
|
+
passes and the lead let go, and a window that stops waits at most one frame.
|
|
2154
|
+
The batches are decided when they are sent, by the scheduler the search marks use: from the ids still waiting, newest first, up to 100 ids a request
|
|
2137
2155
|
and `DAILY_REPORT_SEARCH_BATCHES_IN_FLIGHT` (2) requests at once per API base path (an aborted request keeps its slot until its fetch settles), each
|
|
2138
2156
|
sent `cache: "no-cache"` with a signal of its own.
|
|
2139
|
-
A row that unmounts leaves its load,
|
|
2140
|
-
comes back asks again. A report a 2xx answer lacks reads as `null` (not visible, or deleted), and another status, a body that does not match the
|
|
2141
|
-
schema or a network failure is the rows' error (`[daily-report] reports answered HTTP <status>` for a status)
|
|
2157
|
+
A row that unmounts leaves its load, and the details of a report are loaded once for all of its readers (the rows, the lead, the side pane), so an
|
|
2158
|
+
id nothing waits for is not sent, a request none of whose ids is still waited for is aborted, and a row that comes back asks again. A report a 2xx answer lacks reads as `null` (not visible, or deleted), and another status, a body that does not match the
|
|
2159
|
+
schema or a network failure is the rows' error (`[daily-report] reports answered HTTP <status>` for a status), except over a report a row already
|
|
2160
|
+
shows: a failed refetch of an expired report leaves it in place (the shared detail record under `useDailyReportDetail` below).
|
|
2142
2161
|
A 429 whose `Retry-After` is at most 5 s (it covers a full admission gate's `Retry-After: 1` and stays below the rate bucket's 60 s window) keeps the
|
|
2143
2162
|
rows loading, with no error and no `null`, and asks once more after that delay for the rows still waiting; a second 429, or one without such a
|
|
2144
2163
|
`Retry-After`, is the error.
|
|
@@ -2265,34 +2284,46 @@ While an answer shows, the List cards, the side pane and the DetailList rows mar
|
|
|
2265
2284
|
- **Arrival**: the screen holds one marks store per shown answer (inside the client bundle), only while a view draws an answer that has words: without
|
|
2266
2285
|
`config.search`, before the first answer is drawn, for a filters-only answer (it has no words, so nothing is marked and no match indicator shows),
|
|
2267
2286
|
after the release and for a view rendered on its own there is none, and the DOM is the plain one.
|
|
2268
|
-
A row
|
|
2269
|
-
|
|
2287
|
+
A row hands its marks scope the marks of the unit that drew its card: the row reads the report's details and marks through its input source from
|
|
2288
|
+
the moment it mounts as a shell (the first subscriber of a report asks for its marks) and draws them with its units (**Rows mount in two passes**
|
|
2289
|
+
under [Keyboard, focus and selection](#keyboard-focus-and-selection-list--detaillist)), so a card whose marks are known is drawn with them in its first
|
|
2290
|
+
commit, never plain first, and marks that arrive later are drawn by one unit of the row, which keeps the row's box (the row stays skippable).
|
|
2291
|
+
The desktop side pane reads the marks with a deferred reader instead: its first render after a change of report draws the text plain, and the marks
|
|
2292
|
+
follow in a deferred render.
|
|
2293
|
+
The scope subscribes to nothing: it hands the marks to its leaves through one row context (the marks, their revalidation, the row's tally and the
|
|
2294
|
+
indicator's settings) that only the scope provides, and settles the row's tally in its own layout effect, after its leaves registered;
|
|
2270
2295
|
a leaf, a match indicator or a List card's match description drawn outside a scope while the screen holds a store stops with an error naming the
|
|
2271
2296
|
package, so no leaf draws marks that no tally counts.
|
|
2272
|
-
The
|
|
2273
|
-
|
|
2297
|
+
The card body reads no marks, so an arrival re-renders only the row's container and frame once (one unit) or the side pane's marks scope, then the
|
|
2298
|
+
scope, the marked leaves and the match indicator — never the row's body, header, attachment list or comment section, and never another row.
|
|
2274
2299
|
A leaf registers in its row's tally only once the row's marks have arrived (the indicator and the description say nothing about a row without
|
|
2275
|
-
marks),
|
|
2276
|
-
|
|
2300
|
+
marks), in one layout effect that revalidates, registers and returns its unregistration (an opaque token that removes exactly that registration),
|
|
2301
|
+
so a row that mounts during a held key adds no tally work before its marks, and the tally builds its summary when it is read and tells its readers
|
|
2302
|
+
once, at the end of the task that changed it, however many leaves registered in it: a card's match description is complete by the end of the task
|
|
2303
|
+
that committed the card.
|
|
2304
|
+
While an answer is shown, each view also keeps the rows beyond its rendered range subscribed (the input lead: 24–40 rows beyond each rendered edge,
|
|
2305
|
+
8 more on the side it travels towards whenever fewer than 24 remain, 32 on each side after a jump), so a row that mounts during a held key finds its
|
|
2306
|
+
marks and details in the stores.
|
|
2277
2307
|
A report's first subscription reaches the requests through a batching window of 240 ms: outside a window it is handed on at once, together with the
|
|
2278
2308
|
subscriptions of the same task, and opens a window; the subscriptions made inside the window are handed on together at its end, which opens the next
|
|
2279
|
-
window while
|
|
2280
|
-
|
|
2281
|
-
|
|
2309
|
+
window while first subscriptions keep coming, and a report whose subscribers all left inside the window asks for nothing.
|
|
2310
|
+
During a held key the windows are opened by the lead's chunks of 8 rows, one chunk every 8 keys (about 240 ms at a 30 ms repeat), so a chunk never
|
|
2311
|
+
waits for the window the previous chunk opened, and a 40-key hold sends 4–5 requests per store; a row is the first subscriber of its report only
|
|
2312
|
+
when the lead does not hold it yet (the view's first render, after a jump, or a scroll faster than the lead).
|
|
2282
2313
|
The epoch's renewal, a field's revalidation, a 429's retry and the deferred ids do not wait for the window.
|
|
2283
2314
|
The requests are decided at send time: a subscribed report waits in one queue, and each free request slot — two in flight at most — takes the newest
|
|
2284
|
-
waiting reports that a row
|
|
2285
|
-
|
|
2286
|
-
A request whose
|
|
2315
|
+
waiting reports that are still subscribed (by a row or the lead), at most 100 a request, never cached. A report whose subscribers left while it waited
|
|
2316
|
+
is dropped, so a scroll sends nothing for the rows it passed and the lead let go, and the window it stops on goes out in the first free slot.
|
|
2317
|
+
A request none of whose reports is still subscribed is aborted (the server then stops computing its marks; **Requests** above), and a row that leaves before its
|
|
2287
2318
|
marks were sent, or whose request was aborted, asks again when it returns.
|
|
2288
2319
|
An aborted request keeps its slot until its fetch settles, so the requests in flight, aborted ones included, never exceed the two the server's
|
|
2289
2320
|
per-viewer wait queue is sized for while it still winds the aborted ones down.
|
|
2290
2321
|
The store keeps one record per report (its marks, the send order that brought them, the epoch it was asked in, its 429 retry and its revalidations)
|
|
2291
|
-
and forgets the oldest records of reports
|
|
2292
|
-
many
|
|
2322
|
+
and forgets the oldest records of reports nothing subscribes to beyond 400 (2 × the two requests in flight × 100: the answers still in flight and as
|
|
2323
|
+
many reports just let go), so its memory follows the drawn rows and their lead, not the distance scrolled; a forgotten report asks again when it is
|
|
2324
|
+
subscribed again.
|
|
2293
2325
|
The ids an answer defers (`deferred`, past the server's budget) are asked again through the same scheduler, once the answer arrived in the epoch it
|
|
2294
|
-
was sent and carried at least one report: an id
|
|
2295
|
-
again.
|
|
2326
|
+
was sent and carried at least one report: an id still subscribed is queued again, any other is given back, so a row that returns asks again.
|
|
2296
2327
|
The server always answers a request's first report, so every re-ask makes progress and the re-asks end with the ids asked in the epoch; a deferral
|
|
2297
2328
|
without any report breaks the contract and is not asked again (those rows stay plain for the epoch), and a previous epoch's deferral is not queued
|
|
2298
2329
|
(the epoch's renewal asked again for every subscribed report).
|
|
@@ -2304,7 +2335,7 @@ While an answer shows, the List cards, the side pane and the DetailList rows mar
|
|
|
2304
2335
|
painted and the field asks once for fresh marks. An answer whose `wordsDigest` differs from the shown answer's words is dropped; a quiet re-query of
|
|
2305
2336
|
the same search makes the marks stale and the mounted rows ask again;
|
|
2306
2337
|
401 and 403 call `onSessionExpired`; a 429 whose `Retry-After` is at most 60 s (one window of the server's rate bucket) is asked again once, after
|
|
2307
|
-
that delay, for the reports
|
|
2338
|
+
that delay, for the reports still subscribed then (one whose subscribers left meanwhile asks again when it is subscribed again), and a 429 without a
|
|
2308
2339
|
`Retry-After`, or with a longer one, sets no timer and leaves its reports plain for the answer's epoch (a delay of 2^31 ms or more would overflow
|
|
2309
2340
|
the timer and ask again at once);
|
|
2310
2341
|
any other failure of the request — a network failure, an abort, another status, a body that is not JSON — leaves those rows plain for as long as the
|
|
@@ -2345,9 +2376,13 @@ narrow for the author and the slot the slot drops to a 16 px line of its own ins
|
|
|
2345
2376
|
A DetailList row's description (`aria-describedby`) ends with that element while the screen holds a marks store, so moving through the rows with the
|
|
2346
2377
|
arrow keys reads the sentences each row shows.
|
|
2347
2378
|
- A List card shows no slot. While the screen holds a marks store, the description of its row and of its primary button ends, after the preview,
|
|
2348
|
-
with a `hidden` element inside the card's marks scope (`SearchMatchDescription`, `data-daily-report-search-match-description`) that holds the
|
|
2349
|
-
|
|
2350
|
-
|
|
2379
|
+
with a `hidden` element inside the card's marks scope (`SearchMatchDescription`, `data-daily-report-search-match-description`) that holds the same
|
|
2380
|
+
sentences the DetailList row's indicator holds for the same report under the same answer, built by one rule: the matched keywords, the marked fields
|
|
2381
|
+
and the hidden match (a card whose leaves draw none of the report's matches — a report matched only in the body beyond the preview, for example —
|
|
2382
|
+
says the hidden match in the List, while the DetailList, which draws the whole body, names the content field). The element has no box, so the
|
|
2383
|
+
sentences never change the row's height, and each description reads them once; they are empty until the marks arrive. So the active row's
|
|
2384
|
+
announcement says how many keywords matched, what matched, whether only a reading did and whether the match lies outside the card. Without a store
|
|
2385
|
+
the card's description is unchanged.
|
|
2351
2386
|
|
|
2352
2387
|
## Client wiring
|
|
2353
2388
|
|
|
@@ -2407,14 +2442,29 @@ render (`[daily-report] config.headingLevel must be an integer from 2 to 5 …;
|
|
|
2407
2442
|
`DailyReportPage` is layout-agnostic (header rendering via `renderHeader` slot, error boundaries/footer control managed by the app). For fine-grained
|
|
2408
2443
|
usage, `DailyReportActionProvider`, `DailyReportResolvedContent`, `useDailyReportDetail`, etc. can be imported individually.
|
|
2409
2444
|
Every field `useDailyReportDetail(reportHubId, businessDate)` returns — `report`, `error`, `isLoading` and `isRefetching` — belongs to the id it is
|
|
2410
|
-
called with
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2445
|
+
called with. The details of a report live in one record per report id that every reader of the screen shares — the views' rows, the views' input
|
|
2446
|
+
lead while a search answer is shown, the List's side pane and the hook — so one report has one record and one load whoever reads it: its first
|
|
2447
|
+
reader's subscription starts the load and its last reader's leaving stops it (an answer that arrives after that is dropped), and a cache write
|
|
2448
|
+
reaches the readers of its own report only, once per answer. The hook copies the record into state at default priority. On the first render after
|
|
2449
|
+
the id changes it already returns that id's starting state: its cached report (one definition, also used by the load: the report cache, expired
|
|
2450
|
+
entries included, then the business date's cached list), `isLoading` only when nothing is cached, no error and no refetch yet — never the previous
|
|
2451
|
+
id's report, error or loading flags. An expired cached report is therefore drawn at once, never preceded by `null`, and refetched behind it. An id of
|
|
2452
|
+
0 or less fetches nothing and returns no report (0 also returns an `Invalid ID` error). A row and the side pane that show one report share its
|
|
2453
|
+
record, so they never run two loads of it at once, and readers waiting on one load of a report not shown yet receive its failure once each. A
|
|
2454
|
+
settled report is shared with every later reader while its cache entry is fresh. Once the entry has expired, a reader that subscribes to the record
|
|
2455
|
+
— the side pane, which is rebuilt for every report it shows, a row that mounts, an id that enters the input lead's band — re-validates it after
|
|
2456
|
+
120 ms (the same wait as the refetch below), even while a row keeps the record open: the record keeps the expired report meanwhile (never a loading
|
|
2457
|
+
state), every reader that subscribes before the answer shares the one request, and the answer reaches every reader once; a report selected again
|
|
2458
|
+
inside its TTL is not fetched. A refetch of a shown report that fails (the re-validation, or the first reader's refetch of an expired cached report)
|
|
2459
|
+
leaves the report in place with no error and only logs a warning on `[DailyReportDetailLoader]`, so the next new reader re-validates again. A settled
|
|
2460
|
+
load error is not shared either: a reader that subscribes to a record holding an error — a row that mounts, an id that enters the input lead's band,
|
|
2461
|
+
the side pane — asks again, and the answer replaces the error. Only a new subscription asks again or re-validates (a re-render or a second
|
|
2462
|
+
subscription of the same listener does not, so a row that keeps showing a failure or an expired report never loops), and a refusal of the viewer's
|
|
2463
|
+
session (401 or 403, which cannot succeed before `onSessionExpired` has run) is shared like an answer: it becomes the record's error even over a
|
|
2464
|
+
shown report, and no new reader asks again or re-validates.
|
|
2465
|
+
The first load of a report that is not cached starts inside its first reader's subscription, with no task between the row's mount and its request,
|
|
2466
|
+
so a held arrow key's stream of keydowns cannot postpone it until the key is released; only the refetch of an expired cached report waits 120 ms, to
|
|
2467
|
+
absorb rows that only scroll past. Loads of reports of one business date share one request.
|
|
2418
2468
|
|
|
2419
2469
|
Mutation failures (save / publish / delete / comment) are surfaced through an error seam: inject `config.onError(info: DailyReportErrorInfo)` to route
|
|
2420
2470
|
them into your own toast/notification system, or omit it to use the package's built-in notice. Failures on a continuation that resolves after a
|
|
@@ -2761,7 +2811,8 @@ the resize handle and the side pane lie outside the list.
|
|
|
2761
2811
|
leaving the view and unmounting end it without applying it.
|
|
2762
2812
|
On a slow device the keys therefore never queue up, and the selection stops when the key is released. `Ctrl+Home` / `Ctrl+End` are never
|
|
2763
2813
|
accumulated. While a stream lasts, the desktop side pane keeps showing the report the stream started from (see **List side pane** below), and rows
|
|
2764
|
-
far
|
|
2814
|
+
get their bodies only in the frames without a key, far rows behind the travel direction after the key is released (see **Rows mount in two
|
|
2815
|
+
passes** below).
|
|
2765
2816
|
- **Ignored keys**: `Alt` / `Ctrl` / `Meta` / `Shift` combinations other than `Ctrl+Home` / `Ctrl+End` alone (so `Ctrl+PageDown` and `Shift+End` keep
|
|
2766
2817
|
their browser meaning); an event whose default is already prevented; IME composition (`isComposing` or `keyCode` 229);
|
|
2767
2818
|
keys whose target is neither the view's composite nor a control inside one of its rows (the scroll bar, the side pane, the mobile overlay); the
|
|
@@ -2855,32 +2906,55 @@ the resize handle and the side pane lie outside the list.
|
|
|
2855
2906
|
- **Rows mount in two passes.** A row that a commit mounts outside the visible range first renders as a **shell**: its row frame, with its role,
|
|
2856
2907
|
id, position, set size, level and selection, named by the report's ISO business date and `aria-busy="true"`, and an empty surface
|
|
2857
2908
|
(`data-daily-report-row-shell`) that fills the box `VirtualScroll` gives the row, so nothing moves when the body arrives.
|
|
2858
|
-
One scheduler per view then advances the rows one at a time — a shell to its body, a body that still draws
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
-
|
|
2871
|
-
|
|
2872
|
-
one
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
the
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2909
|
+
One scheduler per view then advances the rows one at a time — a shell to its body, a body that still draws older inputs to the latest ones (a report
|
|
2910
|
+
that arrived by a load's answer or an SSE update, or the report's search marks) — each unit applied with `flushSync` as one commit of its own.
|
|
2911
|
+
A row reads its report's details and search marks through one input source from the moment it mounts, and a unit draws what the source holds when it
|
|
2912
|
+
is applied, so an arriving answer never re-renders a row by itself: a row renders only through its units and its own state (such as its edit form).
|
|
2913
|
+
- **Units commit only in key-free frames.** The scheduler applies units only inside its own frame callback, and only when nothing of a key happened
|
|
2914
|
+
since its previous callback: no `keydown` or `keyup` was dispatched in the view's window (two passive capture listeners that only count, so they
|
|
2915
|
+
read nothing), the held key's stream applied no step in a frame callback, and the stream holds no accumulated step. A unit the scheduler applies
|
|
2916
|
+
in its own frame callback therefore never renders in the frame that renders a key, a single press included; a unit commits elsewhere only in the
|
|
2917
|
+
two cases named below (a row that becomes visible, and a newer input of a visible row outside a key hold). The first callback after the scheduler
|
|
2918
|
+
wakes only calibrates. The scheduler arms itself
|
|
2919
|
+
with a `MessageChannel` message and calls `requestAnimationFrame` only from its own message and callbacks — never from a keydown, a commit or the
|
|
2920
|
+
held key's frame — so the harness never counts its work as a key's.
|
|
2921
|
+
- **A frame's share.** A key-free frame applies its first unit, then more units only while their measured costs (the p90 of the latest 32 per kind
|
|
2922
|
+
of unit, known after 8; kinds are a box sizing and a change: a body over a shell, a change that can change the box, the marks alone) fit half the
|
|
2923
|
+
frame interval and the time left in the frame. At today's costs a DetailList card takes one frame, and the List packs 1–3 rows. A frame whose
|
|
2924
|
+
callbacks started late (a GC or a long task ran before them) skips its first unit when that unit no longer fits (a 2 ms margin); a unit of a
|
|
2925
|
+
visible or reach row is skipped by at most one late frame in a row.
|
|
2926
|
+
- **Order.** By zone — visible, then the active row's reach, then near, then far (**Far rows skip rendering** below) — and within a zone by the
|
|
2927
|
+
distance from the active row, the side the keys travel towards first; equal ranks keep the order the units were added in. While a key is held, far
|
|
2928
|
+
rows ahead in the travel direction are drawn after the near ones, so the rows a hold brings into the reach already have their bodies; far rows
|
|
2929
|
+
behind wait for the key's release.
|
|
2930
|
+
- **Visible rows are never stale.** A row that becomes visible with a pending unit renders its body inside the commit that made it visible, so a
|
|
2931
|
+
visible row is never a shell and never shows outdated data or marks (counted). Outside a key hold, a newer input of a row that is already visible
|
|
2932
|
+
is drawn at once: in the commit that brings it, or, when it arrives as a store notification, in one commit at the end of the notifying task.
|
|
2933
|
+
- **Search answers lead the rows' inputs.** While a search answer is shown, each view keeps its rendered rows and 24–40 rows beyond each rendered
|
|
2934
|
+
edge subscribed to the details and the marks (8 more rows on the side it travels towards whenever fewer than 24 remain, 32 on each side after a
|
|
2935
|
+
jump), from its range report, which `VirtualScroll` delivers in a frame callback, never inside a keydown. A row that mounts during a held key finds
|
|
2936
|
+
its details and marks already loaded, so every unit of a hold draws one complete card with its marks, and the requests leave in chunks (4–5 per
|
|
2937
|
+
store in a 40-key hold; after a jump one request per store carries the rendered window and 2 × 32 rows, at most 96 ids). The lead holds its
|
|
2938
|
+
subscriptions per store and releases only the swapped store's: a new answer (a new marks store) re-leases the marks and keeps the details and their
|
|
2939
|
+
request in flight, so it never asks for the same ids twice; a new details store also forgets the edges and leases as after a jump. The by-date
|
|
2940
|
+
views keep the by-date prefetch.
|
|
2941
|
+
- **Heights settle in the frame that lays the body out.** The DetailList's row-height delivery — one `ResizeObserver` per view, made from the
|
|
2942
|
+
view's own window where its scroll container attaches, beside the scheduler, and disconnected when the container detaches — commits each delivery
|
|
2943
|
+
in one synchronous commit:
|
|
2944
|
+
the row's height, `VirtualScroll`'s positions (with the compensation above the visible start) and the row's settled state land in the frame that
|
|
2945
|
+
laid the body out. A row that this commit reveals, or a body whose box it changes, is measured before the delivery ends and observed at the
|
|
2946
|
+
view's next frame callback, so no observation starts inside a delivery and the browser never reports "ResizeObserver loop completed with
|
|
2947
|
+
undelivered notifications".
|
|
2948
|
+
With a 30 ms key repeat at 60 Hz, a hold has about 0.8 key-free frames per key and brings about one row per key into the reach, so a 40-key hold
|
|
2949
|
+
keeps several bodied rows ahead of the active row; holds longer than about 60 keys draw the bodies they reveal inside the key's own commit (one
|
|
2950
|
+
complete card each, counted).
|
|
2951
|
+
On a slow device (the CPU slowed 4×) a held key renders about 9 frames a second — the next frame's p95 is about 109 ms against the 33 ms target —
|
|
2952
|
+
because each key frame's own style, layout and paint take that long; every row that enters the visible range is still complete, with its card and
|
|
2953
|
+
marks, at its entry.
|
|
2954
|
+
Decisions never wait for the drawing: the card and the error surface are both drawn from the unit's snapshot, and only the safety valve that
|
|
2955
|
+
removes a row whose report no longer exists, the busy state of a row whose card or error waits for its unit (a load error that arrived but is not
|
|
2956
|
+
drawn yet keeps the row's skeleton, never a "not found" surface), and a DetailList body's draft and ownership, its opening of the viewer's own draft
|
|
2957
|
+
in the editor and its edit button's action read the row's current details.
|
|
2884
2958
|
- **Far rows skip rendering.** Every mounted row lies in one zone, computed in the render that positions the rows from that render's own scroll
|
|
2885
2959
|
position (half-open intervals in content pixels, V the viewport's size, s the scroll position): **visible** when it intersects [s, s + V);
|
|
2886
2960
|
**reach** when it intersects the active row's reach, from V above the row before the active one to V below the row after it — every row that a
|
|
@@ -2999,7 +3073,10 @@ the resize handle and the side pane lie outside the list.
|
|
|
2999
3073
|
(re-recording there would read the clamped position against the previous list's rows, name another report and leave the view displaced by up to
|
|
3000
3074
|
the last row's height).
|
|
3001
3075
|
- **Scrolls `VirtualScroll` makes for the user** (wheel, drag, the scroll bar, inertia) are recorded each time the view reports its visible range,
|
|
3002
|
-
once per frame. A
|
|
3076
|
+
once per frame. A fling far beyond the DetailList's rendered window is not symmetric: the rows it passes as shells are drawn above the pane
|
|
3077
|
+
afterwards and grow from the unmeasured estimate (352 px, below every measured row) to their heights, and `VirtualScroll`'s compensation keeps the
|
|
3078
|
+
visible rows in place, so wheeling back by the same distance lands several hundred px (up to about 1,100) away from where the fling started; the
|
|
3079
|
+
visible rows never jump. A list change in the frame before that report first advances the anchor by the distance scrolled since it was recorded and
|
|
3003
3080
|
restores from there: it neither undoes the user's scroll nor shifts the content by a row.
|
|
3004
3081
|
Over rows of one height (the List) the new anchor follows from the distance by arithmetic; over measured rows (the DetailList) it walks the
|
|
3005
3082
|
previous list's row heights from the anchor to the report now at the top, so the walk is bounded by the rows passed since the record (one frame's
|
|
@@ -3058,7 +3135,12 @@ the resize handle and the side pane lie outside the list.
|
|
|
3058
3135
|
rows are the whole set; the views' `VirtualScroll` always counts the rows given.
|
|
3059
3136
|
The markers are the attachment marker (`<labels.attachments>: <count>`) and the source badge (its full `name` when the configuration gives one,
|
|
3060
3137
|
otherwise its label); a description references only the markers that are painted. A shell, and a row without content (loading, failed, missing,
|
|
3061
|
-
editing), is named by its ISO business date, with `aria-busy="true"` while its report or its body is still to come
|
|
3138
|
+
editing), is named by its ISO business date, with `aria-busy="true"` while its report or its body is still to come, and is described by the state
|
|
3139
|
+
it shows, so the active descendant's announcement never reduces a visible state to a date: a failed row by its error message
|
|
3140
|
+
(`labels.reportSummaryLoadFailed` in the List, `labels.reportLoadFailed` in the DetailList), a List row the server did not find by
|
|
3141
|
+
`labels.reportNotFound`, and a DetailList row that shows its edit form — the viewer's own draft opens there by itself — by a visually hidden
|
|
3142
|
+
`labels.rowEditingState({ isDraft })` (en "Editing draft" / "Editing", ja 「下書きを編集中」 / 「編集中」). A shell and a loading skeleton have no
|
|
3143
|
+
description. ★, 既読, 編集 and 削除 carry
|
|
3062
3144
|
`aria-label` equal to their `title` and no `aria-pressed`; no element of a view carries `aria-current`.
|
|
3063
3145
|
The active row is never skipped, so its name and description are always complete; a far row that skips rendering is absent from the accessibility
|
|
3064
3146
|
tree (**Far rows skip rendering** above). The package's tests compute the active row's name and description in both views; how real screen readers
|
|
@@ -3935,7 +4017,11 @@ a key's render the frame already names the new selection while the content still
|
|
|
3935
4017
|
`[data-testid="daily-report-list"]` and `[data-testid="daily-report-detail-list"]` carry a `__virtualScroll` accessor for end-to-end tests, with the
|
|
3936
4018
|
same shape in both views. It is the read-only part of the current `VirtualScroll` handle — `getViewportSize()`, `getScrollPosition()`,
|
|
3937
4019
|
`getScrollAnchor()`, `getRange()` and `getFenwickSize()` — plus `findReportIndex(id)`, `getReportItem(index)` and `getReportIds(limit = 20)` over the
|
|
3938
|
-
view's committed list,
|
|
4020
|
+
view's committed list, one way to move the position, `revealIndex(index, options)`, and one read-only view of the view's row pass,
|
|
4021
|
+
`rowPass.counters()`: the counts since the view mounted, read when it is called — `syncReleases` (units drawn synchronously, in a commit or a
|
|
4022
|
+
notification's microtask commit), `oversizeFrames` (key-free frames whose first unit alone exceeded the frame's share), `lateFrames` (late key-free
|
|
4023
|
+
frames the pass skipped) and `lateForcedFrames` (late key-free frames it applied a unit in regardless). A performance probe reads them before and
|
|
4024
|
+
after a key hold and reports the differences, so it copies none of the pass's constants or rules (**Rows mount in two passes**).
|
|
3939
4025
|
The client entry exports the contract as types, so a host's tests import it instead of restating it (type-only, no bundle bytes):
|
|
3940
4026
|
`DailyReportViewTestHandle` is the accessor, `DailyReportViewTestReadHandle` its read-only part, and `DailyReportRevealOptions` the options of
|
|
3941
4027
|
`revealIndex`, which are the second argument of `VirtualScroll`'s `scrollToIndex` (its alignment and offset, as the peer defines them).
|
|
@@ -3960,9 +4046,9 @@ locale**: English (`"en"`, the default) and Japanese (`"ja"`). Pick the language
|
|
|
3960
4046
|
/>
|
|
3961
4047
|
```
|
|
3962
4048
|
|
|
3963
|
-
- **One surface for all wording.** The catalog covers the 11 engine chrome keys of `@aiquants/virtualscroll` plus
|
|
3964
|
-
page title (`title`, passed to `renderHeader` and used as the accessible name of both lists), the lists' keyboard help, the row
|
|
3965
|
-
card's position read to screen readers, tabs, buttons, placeholders, the `confirm()` text, stream badges, the search box, its filter row and the
|
|
4049
|
+
- **One surface for all wording.** The catalog covers the 11 engine chrome keys of `@aiquants/virtualscroll` plus 111 own keys: field headings, the
|
|
4050
|
+
page title (`title`, passed to `renderHeader` and used as the accessible name of both lists), the lists' keyboard help, the row states (read and
|
|
4051
|
+
starred, editing) and the List card's position read to screen readers, tabs, buttons, placeholders, the `confirm()` text, stream badges, the search box, its filter row and the
|
|
3966
4052
|
search's match indicator, the status screen and the page's status region and the mutation-failure message with its repeat count. The engine part of
|
|
3967
4053
|
each catalog is `@aiquants/virtualscroll`'s own catalog of the same locale, reused by value, so the scroll arrows, the names of the scroll bar and
|
|
3968
4054
|
its thumb, and the "No items" text always speak the same language as the rest.
|
|
@@ -3970,12 +4056,12 @@ locale**: English (`"en"`, the default) and Japanese (`"ja"`). Pick the language
|
|
|
3970
4056
|
and the 11 engine keys of the resolved labels to their `VirtualScroll`. The engine label object keeps
|
|
3971
4057
|
its identity while its values are unchanged, so rebuilding `config.labels` on every render does not
|
|
3972
4058
|
re-render the memoized list subtree.
|
|
3973
|
-
- **Formatter keys.** Twenty-
|
|
4059
|
+
- **Formatter keys.** Twenty-two keys take arguments and are functions: `totalCount(count)`,
|
|
3974
4060
|
`debounce(milliseconds)`, `streamProgressCount(loaded)`, `streamProgressRatio(loaded, total, percent)`,
|
|
3975
4061
|
`streamRetrying(loaded)`, `reportLoadFailed(reportHubId)`, `reportSummaryLoadFailed(reportHubId)`,
|
|
3976
4062
|
`interviewerWithAffiliation(name, affiliation)`, `operationFailed(operation)` (`operation` is a
|
|
3977
4063
|
`DailyReportOperation`: `create` / `update` / `publish` / `delete` / `addComment` / `deleteComment` /
|
|
3978
|
-
`toggleStar` / `toggleRead`), `noticeRepeated(count)`, `rowState({ isRead, isStarred })`, `listRowPosition(position, total)`,
|
|
4064
|
+
`toggleStar` / `toggleRead`), `noticeRepeated(count)`, `rowState({ isRead, isStarred })`, `rowEditingState({ isDraft })`, `listRowPosition(position, total)`,
|
|
3979
4065
|
`listRowPositionInUnknownTotal(position)`,
|
|
3980
4066
|
`searchHitCount(count)`, `searchFailed(failure)` (`failure` is `invalid` / `busy` / `unavailable` / `failed`),
|
|
3981
4067
|
`searchMatchedKeywords(matched, total)`, `searchMatchedKeywordsDescription(matched, total)`, `searchFieldMatched(field)` and
|
|
@@ -3986,7 +4072,7 @@ locale**: English (`"en"`, the default) and Japanese (`"ja"`). Pick the language
|
|
|
3986
4072
|
locale built once (the row positions and counts re-render on every stream publish); a host override receives
|
|
3987
4073
|
the raw number and formats it itself.
|
|
3988
4074
|
- **Fail-fast**: an unsupported `locale` (`"EN"`, `"ja-JP"`, `"fr"`, `""`, `null`, ...) throws a
|
|
3989
|
-
`RangeError` at render. So do an unknown `labels` key (a key error that lists the
|
|
4075
|
+
`RangeError` at render. So do an unknown `labels` key (a key error that lists the 122 keys), a string
|
|
3990
4076
|
key whose value is not a string with non-whitespace content, and a formatter key whose value is not a
|
|
3991
4077
|
function. An `undefined` value keeps the catalog value.
|
|
3992
4078
|
- **`config` has a closed key set**: `apiBasePath`, `ssePath`, `locale`, `labels`,
|
|
@@ -4024,7 +4110,7 @@ locale**: English (`"en"`, the default) and Japanese (`"ja"`). Pick the language
|
|
|
4024
4110
|
grouping follows `locale` (`en` → `"en-US"`, `ja` → `"ja-JP"`) and dates stay in ISO form. Packages
|
|
4025
4111
|
that do format dates (for example `@aiquants/period-slider`) name that separate axis `formatLocale`.
|
|
4026
4112
|
|
|
4027
|
-
Exported API (`@aiquants/daily-report/client`): `DAILY_REPORT_LABEL_KEYS` (all
|
|
4113
|
+
Exported API (`@aiquants/daily-report/client`): `DAILY_REPORT_LABEL_KEYS` (all 122 keys, the 11 engine keys
|
|
4028
4114
|
first), `DAILY_REPORT_LABEL_CATALOGS` (the frozen catalogs), `resolveDailyReportLabels(locale, labels)`
|
|
4029
4115
|
(the effective frozen labels — the catalog object itself when `labels` is `undefined`), the resolved
|
|
4030
4116
|
`locale` / `labels` on `useDailyReportConfig()`, and the types `DailyReportLocale` (= `VirtualScrollLocale`),
|
|
@@ -4130,6 +4216,7 @@ first), `DAILY_REPORT_LABEL_CATALOGS` (the frozen catalogs), `resolveDailyReport
|
|
|
4130
4216
|
| `operationFailed` | the error notice / `DailyReportErrorInfo.message` | `("update")` → Failed to save the report. Please try again later. | `("update")` → 日報の保存に失敗しました。時間をおいて再度お試しください。 |
|
|
4131
4217
|
| `noticeRepeated` | the error notice, beside its message when failures with the same message follow one another (`count` is how many came in a row, from 2; part of the notice's name) | `(2)` → (2 times) | `(2)` → (2 回目) |
|
|
4132
4218
|
| `rowState` | visually hidden row state, the first description of a card row in both views and the second of the List card's primary button | `({ isRead: false, isStarred: true })` → Unread, starred | `({ isRead: false, isStarred: true })` → 未読、スター付き |
|
|
4219
|
+
| `rowEditingState` | visually hidden state of a DetailList row that shows its edit form in place of the card (the viewer's own draft opens there by itself), the row's description | `({ isDraft: true })` → Editing draft; `({ isDraft: false })` → Editing | `({ isDraft: true })` → 下書きを編集中; `({ isDraft: false })` → 編集中 |
|
|
4133
4220
|
| `listRowPosition` | visually hidden position of a List card in the list, the first description of its primary button (`position` is 1-based; `total` is the size of the views' set) | `(3, 1234)` → 3 of 1,234 | `(3, 1234)` → 全 1,234 件中 3 件目 |
|
|
4134
4221
|
| `listRowPositionInUnknownTotal` | the same position while the size of the views' set is unknown (the ids stream has not declared its total yet), in place of `listRowPosition`; it also ends the description of a card row in both views while the size is unknown | `(3)` → Item 3 | `(3)` → 3 件目 |
|
|
4135
4222
|
| `searchFieldMatched` | match sentences of a row (the DetailList row's and the side pane's hidden indicator sentences, the List card's hidden match description): a field heading the row's leaves marked with a typed word's mark (`field` is the heading's resolved label) | `("Author")` → Author matched | `("作成者")` → 作成者で一致 |
|
|
@@ -4307,6 +4394,8 @@ Documenting a name here is no reason to export it, and the values that tune the
|
|
|
4307
4394
|
session's client is not public. The route helpers `createDailyReportClientLoader` / `dailyReportShouldRevalidate`.
|
|
4308
4395
|
- Configuration and wording: `defaultDailyReportClientConfig` / `resolveDailyReportLabels` / `DAILY_REPORT_LABEL_CATALOGS` /
|
|
4309
4396
|
`DAILY_REPORT_LABEL_KEYS` / `resolveSourceTypeConfig`.
|
|
4397
|
+
- The source-type fold `foldDailyReportSourceTypeToken`, the same function as the shared entry's, for the host's route modules that ship to the
|
|
4398
|
+
browser (the shared entry is not split, so a route module that imports the fold from it carries a second copy of shared modules into its chunk).
|
|
4310
4399
|
- The search's filter controls: `DAILY_REPORT_SEARCH_FILTER_FACETS` with its type `DailyReportSearchFilterFacet` (the filters in the filter row's
|
|
4311
4400
|
order, `author`, `sourceType` and `customer`), and the contract of the control a host renders in each filter cell:
|
|
4312
4401
|
`DailyReportSearchFilterSelectRenderer` (what `config.search.renderFilterSelect` is) /
|
|
@@ -4344,7 +4433,7 @@ Documenting a name here is no reason to export it, and the values that tune the
|
|
|
4344
4433
|
decode-work and walk bounds, for a host's conformance checks) / `DAILY_REPORT_ATTACHMENT_THUMBNAIL_OUTPUT_MEDIA_TYPES` /
|
|
4345
4434
|
`DAILY_REPORT_ATTACHMENT_THUMBNAIL_MAX_OUTPUT_BYTES`.
|
|
4346
4435
|
- SSE and caching: `buildDailyReportSseStreamKey(streamKey)` (the Redis stream key the service writes and reads the SSE entries of, the base key
|
|
4347
|
-
with the wire version appended, `<streamKey>:
|
|
4436
|
+
with the wire version appended, `<streamKey>:v4`, for a host that reads or writes the stream itself) / `DailyReportSseReader` /
|
|
4348
4437
|
`readDailyReportSseStreamEntry(raw, logger)` (reads one Redis stream entry, `{ id, message }` as the client returns it, once: `{ entry, json }`,
|
|
4349
4438
|
the frozen `StreamEntry` the shared reader hands every subscriber and the entry's parsed JSON value; for a host whose own reader keeps the same
|
|
4350
4439
|
contract) /
|
|
@@ -4369,7 +4458,10 @@ Documenting a name here is no reason to export it, and the values that tune the
|
|
|
4369
4458
|
the ports' own types `DailyReportResolveUserId` / `DailyReportEncodeUserId` / `DailyReportResolveVisibleSourceTypes` (with
|
|
4370
4459
|
`DailyReportVisibleSourceTypes`, `DailyReportVisibilityDimensions`, `DailyReportVisibilityOptOut` (the explicit opt-out
|
|
4371
4460
|
`{ unrestricted: <reason> }`), `DailyReportVisibilityResolveContext` and `DailyReportVisibilityResolveReason`) / `DailyReportRedisClient` / `DailyReportRedisBlockingClient` / `DailyReportRedisStreamMessage` /
|
|
4372
|
-
`DailyReportSseEntryMessage` (an entry's message read once
|
|
4461
|
+
`DailyReportSseEntryMessage` (an entry's message read once: `parsed`, a `DailyReportSseStreamMessage` — the message as the service writes it to
|
|
4462
|
+
the stream, read by `dailyReportSseStreamMessageSchema`: the types that may be addressed carry the server-only `recipientRawUserId`, and the types
|
|
4463
|
+
that embed a report also the server-only `actorRawUserId`, which the delivery replaces with each viewer's `viewerIsActor` — and `published`, the JSON
|
|
4464
|
+
value as written, which only the recipient filter reads; every frame is built from `parsed`) / `DailyReportExternalSource` (an `externalSources` adapter),
|
|
4373
4465
|
the injected tables `DailyReportHubTable` / `DailyReportInternalTable` / `DailyReportCommentTable` / `DailyReportLabelTable` /
|
|
4374
4466
|
`DailyReportHubLabelTable` / `DailyReportUserStatusTable` / `DailyReportUserTable` / `DailyReportAttachmentTable` (the members of
|
|
4375
4467
|
`DailyReportTables`), the service's rows `DailyReportUserStatusRow` / `DailyReportIdsSource`,
|