@aiquants/contact-map 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -8
- package/dist/client.d.mts +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/client.js +1 -1
- package/dist/client.mjs +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/server.d.mts +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +4 -4
- package/dist/server.mjs +4 -4
- package/package.json +5 -5
- /package/dist/{wire-BghOEeyw.d.mts → wire-CVZNwmQx.d.mts} +0 -0
- /package/dist/{wire-BghOEeyw.d.ts → wire-CVZNwmQx.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -33,8 +33,8 @@ Reusable, host-agnostic business contact map and corporate directory feature pac
|
|
|
33
33
|
- **Server Layer (`@aiquants/contact-map/server`)**:
|
|
34
34
|
- Dependency Injection (DI) port architecture decouples database connections, schemas, authentication, and loggers from host apps.
|
|
35
35
|
- DB接続、スキーマ、認証、ロガーをホストアプリから疎結合化する DI ポート設計。
|
|
36
|
-
- Streaming keyset pagination handler (`createContactMapStreamHandler`)
|
|
37
|
-
-
|
|
36
|
+
- Streaming keyset pagination handler (`createContactMapStreamHandler`) whose resume cursor keeps `contacted_at` to the 100 ns of `datetime2(7)`, so no row at a chunk boundary is skipped or sent twice.
|
|
37
|
+
- 再開カーソルが `contacted_at` を `datetime2(7)` の 100 ns まで保ち、チャンクの境界の行を読み飛ばしも重複もしないストリーミングキーセットパジネーションハンドラー。
|
|
38
38
|
|
|
39
39
|
---
|
|
40
40
|
|
|
@@ -105,12 +105,26 @@ export async function loader({ request }: { request: Request }) {
|
|
|
105
105
|
}
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
-
`db`, `table`, `ensureConnection` and `authenticate` are required, and `createContactMapStreamHandler` / `createContactMapServer` throw a `TypeError` when `db` or `table` is missing, `
|
|
109
|
-
|
|
108
|
+
`db`, `table`, `ensureConnection` and `authenticate` are required, and `createContactMapStreamHandler` / `createContactMapServer` throw a `TypeError` when `db` or `table` is missing, `table.contacted_at` is missing or is not a Drizzle column, `ensureConnection` or `authenticate` is not a function, or `firstBatchSize` / `nextBatchSize` is given but is not a number.
|
|
109
|
+
They throw a `RangeError` when `table.contacted_at` is not a `datetime2` column (`datetime2` or `datetime2(0)` to `datetime2(7)`; a `datetime`, `datetimeoffset` or `date` column is refused) or `firstBatchSize` or `nextBatchSize` is a number but not an integer from 1 to `Number.MAX_SAFE_INTEGER`.
|
|
110
|
+
Each message names the setting and ends with the value given (`[contact-map] server config authenticate must be the function that returns the request's user (…); got undefined`, `[contact-map] server config table.contacted_at must be a Drizzle datetime2 column; got "datetime"`). A stream open to everyone says so with an `authenticate` that returns a user; leaving the setting out never serves the contacts unauthenticated.
|
|
110
111
|
The handler calls `authenticate(request, { failureRedirect: null })` after checking the resume cursor (a malformed cursor gets 400 without authenticating), answers 401 when no user comes back, and sends a returned `cookie` as `Set-Cookie` on the 401 and on the stream response alike, so a session the call refreshed reaches the browser. The stream is sent with `Cache-Control: no-store, no-transform`.
|
|
111
112
|
|
|
112
|
-
|
|
113
|
-
|
|
113
|
+
The resume cursor (`StreamCursor`: each chunk line's `nextCursor`, sent back as `cursor_contacted_at` / `cursor_person_id`) is written by the server and opaque to clients. Its `contacted_at` is the last row's value in UTC with exactly 7 fractional digits (`YYYY-MM-DDThh:mm:ss.fffffffZ`, the 100 ns of `datetime2(7)`), read from the database as text so that no digit is lost to the driver's millisecond `Date`, or `null` once the stream has reached the rows without one; its `person_id` is the last row's id, any safe integer (a signed key may be negative).
|
|
114
|
+
The chunk query checks both values by the rules below when it writes the cursor, so the next request accepts every cursor the server writes; a chunk whose last row's `person_id` is not a safe integer fails with an error naming the value.
|
|
115
|
+
The handler accepts only that form naming a real instant from year 1 on and a `cursor_person_id` holding a safe integer written in canonical decimal (`0`, or digits without a leading zero after an optional minus sign); anything else (`-0`, `+5`, `05`, `1e3`, …) gets 400 before authenticating.
|
|
116
|
+
The next chunk compares the bare column with the cursor cast to `datetime2(7)`, so an index on (`is_active`, `contacted_at`, `person_id`) can seek. `createContactMapServer().getContactsStreamChunk(cursor)` checks the cursor by the same rules and throws a `TypeError` (a value of the wrong type or shape) or a `RangeError` (any other form) before connecting.
|
|
117
|
+
`ContactMapStreamClient` sends the cursor back unchanged. When a resumed request gets 400 (the cursor was written by a server of another version), it drops the cursor and loads from the beginning again once, keeping the records it has (they are keyed by `person_id`); a 400 to a request without a cursor, or a second 400 in the same load, ends the load at once with `[contact-map] the server rejected the stream request (400); reload the page`.
|
|
118
|
+
A 401 or 403 ends the load at once in phase `auth-required` with the error `Authentication required`, without retrying (`retryNow()` starts again from the last cursor, for example after the user signs in again). Any other failed response is retried with backoff and sets the error to `[contact-map] the stream request failed (status <status>)`.
|
|
119
|
+
|
|
120
|
+
`db`・`table`・`ensureConnection`・`authenticate` は必須で、`db` か `table` が無い、`table.contacted_at` が無いか Drizzle の列でない、`ensureConnection` か `authenticate` が関数でない、または `firstBatchSize` / `nextBatchSize` が数値でなければ `createContactMapStreamHandler` / `createContactMapServer` は `TypeError` を、`table.contacted_at` が `datetime2` の列 (`datetime2` か `datetime2(0)` から `datetime2(7)`。`datetime`・`datetimeoffset`・`date` の列は断る) でないか、`firstBatchSize` か `nextBatchSize` が 1 から `Number.MAX_SAFE_INTEGER` までの整数でない数値なら `RangeError` を投げる。
|
|
121
|
+
どの文言も設定を名指し、与えた値で終わる (`[contact-map] server config authenticate must be the function that returns the request's user (…); got undefined`、`[contact-map] server config table.contacted_at must be a Drizzle datetime2 column; got "datetime"`)。誰にでも開くストリームは user を返す `authenticate` で明示し、設定を書き漏らしても認証なしで連絡先を配ることはない。ハンドラーは再開カーソルを検査してから (不正な形は認証せずに 400) `authenticate(request, { failureRedirect: null })` を呼び、user が無ければ 401 を返し、返された `cookie` は 401 にもストリームの応答にも `Set-Cookie` で載せる (呼び出しが更新したセッションをブラウザへ届ける)。ストリームは `Cache-Control: no-store, no-transform` で送る。
|
|
122
|
+
|
|
123
|
+
再開カーソル (`StreamCursor`。各チャンク行の `nextCursor` で、`cursor_contacted_at` / `cursor_person_id` として送り返す) はサーバーが書き、クライアントにとって形は不透明。`contacted_at` は最後の行の値を UTC で小数部ちょうど 7 桁 (`YYYY-MM-DDThh:mm:ss.fffffffZ`、`datetime2(7)` の 100 ns) で表したもので、ドライバのミリ秒までの `Date` で桁を落とさないようデータベースから文字列で読む。日時の無い行へ進んだ後は `null`。`person_id` は最後の行の ID で、負を含む任意の安全な整数 (符号付きのキーは負にもなる)。
|
|
124
|
+
チャンクの照会はカーソルを書くときに両方の値を下の規則で確かめるので、サーバーが書いたカーソルは次の要求が必ず受け入れる。最後の行の `person_id` が安全な整数でないチャンクは、その値を示すエラーで失敗する。
|
|
125
|
+
ハンドラーは 1 年以降の実在する日時を表すこの形と、安全な整数を正規の 10 進 (`0` か、省略できる負の符号に続く先頭が 0 でない数字) で書いた `cursor_person_id` だけを受け入れ、それ以外 (`-0`・`+5`・`05`・`1e3` など) は認証の前に 400 で断る。次のチャンクは素の列と `datetime2(7)` へ CAST したカーソルを比べるので、(`is_active`, `contacted_at`, `person_id`) の索引でシークできる。`createContactMapServer().getContactsStreamChunk(cursor)` も同じ規則でカーソルを検査し、接続の前に `TypeError` (型や形の誤り) か `RangeError` (それ以外の形) を投げる。
|
|
126
|
+
`ContactMapStreamClient` はカーソルをそのまま送り返す。再開した要求が 400 を受けたら (別の版のサーバーが書いたカーソル)、カーソルを捨てて最初から 1 回だけ読み直し、受信済みのレコードは残す (`person_id` で重ねる)。カーソルの無い要求への 400 と、同じ読み込みでの 2 回目の 400 は、`[contact-map] the server rejected the stream request (400); reload the page` で即座に読み込みを終える。
|
|
127
|
+
401 と 403 は再試行せず、段階 `auth-required`・エラー `Authentication required` で即座に読み込みを終える (`retryNow()` で最終カーソルから読み直す。たとえば再ログインの後)。それ以外の失敗の応答はバックオフで再試行し、エラーを `[contact-map] the stream request failed (status <status>)` にする。
|
|
114
128
|
|
|
115
129
|
### 2. Client Page Setup / クライアント側ページの設定
|
|
116
130
|
|
|
@@ -252,7 +266,7 @@ export default function ContactMapRoute() {
|
|
|
252
266
|
- An own key outside `CONTACT_MAP_LABEL_KEYS` throws a short `RangeError` that suggests the nearest key (`[contact-map] labels has a key that is not a label key (did you mean "searchTab"? CONTACT_MAP_LABEL_KEYS lists every key); got "serchTab"`).
|
|
253
267
|
- A string key must be a string (else a `TypeError`) with non-whitespace content (else a `RangeError`); a function key (the keys shown with arguments in the table below) must be a function (else a `TypeError`). An explicit `undefined` keeps the catalog value.
|
|
254
268
|
- Every error the package throws starts with `[contact-map]`, with one exception: a cancelled query (`countActiveContactsQuery`, `getContactsStreamChunkQuery`) rejects with the standard `DOMException` named `AbortError`, as `fetch` does, so tell a cancellation by `error.name === "AbortError"`.
|
|
255
|
-
- A setting check (`locale`, `labels`, `streamEndpoint`, `workerUrls`, the server config, row counts, tree names) throws a `TypeError` for a value of the wrong type or shape, a missing required setting included, and a `RangeError` for a value of the right type that is not allowed, a key the setting does not take included.
|
|
269
|
+
- A setting check (`locale`, `labels`, `streamEndpoint`, `workerUrls`, the server config, row counts, the resume cursor given to `getContactsStreamChunkQuery` or `createContactMapServer().getContactsStreamChunk` (both named `getContactsStreamChunkQuery cursor` in the message), tree names) throws a `TypeError` for a value of the wrong type or shape, a missing required setting included, and a `RangeError` for a value of the right type that is not allowed, a key the setting does not take included.
|
|
256
270
|
It names the setting as the surface calls it (`labels`, `ContactMapAppView labels`, `server config firstBatchSize`), and it ends with `; got <value>`: the rejected value, or the rejected key, written the same way everywhere (a quoted string, `undefined`, `null`, `-0`, `10n`, `Symbol(x)`, `[object URL]`).
|
|
257
271
|
- Two views of one browser tab that disagree on `streamEndpoint` or `workerUrls.treeWorker`, and a CommonJS build without `workerUrls.treeWorker`, throw a plain `Error` (no single value is wrong).
|
|
258
272
|
- A unit key (`recordUnit`, `companyUnit`, `resultUnit`, `timesUnit`, `itemsUnit`, `charactersUnit`) receives the number exactly as it is displayed (`"1"`, `"1,001"`, `"1.0"`), so the English singular / plural follows what the reader sees: only a displayed `"1"` is singular (`"1.0 items"`). `timesUnit` / `itemsUnit` / `charactersUnit` / `throughputUnit` are appended to the number without a separator and carry their own leading space.
|
|
@@ -263,7 +277,7 @@ export default function ContactMapRoute() {
|
|
|
263
277
|
- `CONTACT_MAP_LABEL_KEYS` 外の自身のキーは、最も近いキーを示す短い `RangeError` (`[contact-map] labels has a key that is not a label key (did you mean "searchTab"? CONTACT_MAP_LABEL_KEYS lists every key); got "serchTab"`)。
|
|
264
278
|
- 文字列キーは文字列 (でなければ `TypeError`) で空白以外を含むこと (でなければ `RangeError`)、関数キー (下表で引数付きのキー) は関数であること (でなければ `TypeError`) が必須。明示 `undefined` はカタログ値のまま。
|
|
265
279
|
- パッケージが投げる例外は全て `[contact-map]` で始まる。例外は 1 つで、取り消した照会 (`countActiveContactsQuery`・`getContactsStreamChunkQuery`) は `fetch` と同じく名前が `AbortError` の標準の `DOMException` で拒否するので、取消は `error.name === "AbortError"` で見分ける。
|
|
266
|
-
- 設定の検査 (`locale`・`labels`・`streamEndpoint`・`workerUrls
|
|
280
|
+
- 設定の検査 (`locale`・`labels`・`streamEndpoint`・`workerUrls`・サーバーの設定・行数・`getContactsStreamChunkQuery` か `createContactMapServer().getContactsStreamChunk` に渡す再開カーソル (文言ではどちらも `getContactsStreamChunkQuery cursor`)・代替名) は、型や形の誤り (必須の設定が無い場合を含む) を `TypeError`、型は合うが許されない値 (設定が受け取らないキーを含む) を `RangeError` にする。設定は面の呼び名で名指し (`labels`・`ContactMapAppView labels`・`server config firstBatchSize`)、末尾は `; got <値>` で、拒否した値か拒否したキーをどこでも同じ表記で書く (引用符付きの文字列・`undefined`・`null`・`-0`・`10n`・`Symbol(x)`・`[object URL]`)。
|
|
267
281
|
- ブラウザのタブの 2 つのビューが `streamEndpoint` か `workerUrls.treeWorker` で食い違う場合と、`workerUrls.treeWorker` の無い CommonJS のビルドは、どの値 1 つが誤りでもないので素の `Error`。
|
|
268
282
|
- 単位キー (`recordUnit`、`companyUnit`、`resultUnit`、`timesUnit`、`itemsUnit`、`charactersUnit`) は表示どおりの数値文字列 (`"1"`、`"1,001"`、`"1.0"`) を受け取るため、英語の単数 / 複数は読み手が見る値に従う (単数は表示 `"1"` のときだけで `"1.0 items"`)。`timesUnit` / `itemsUnit` / `charactersUnit` / `throughputUnit` は数値の直後に区切りなしで連結されるため、先頭の空白は単位側が持つ。
|
|
269
283
|
- エンジン 7 キーと行種別語 2 キーも、`@aiquants/virtualscroll` と `@aiquants/directory-tree` へ渡す前にここで同じ規則で検査するので、その例外もパッケージの規則に従う。
|
package/dist/client.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react from "react";
|
|
2
2
|
import { CSSProperties, ReactNode, RefObject, PointerEvent, MouseEvent } from "react";
|
|
3
3
|
import { UseFuzzySearchOptions } from "@aiquants/fuzzy-search/react";
|
|
4
|
-
import { C as ContactRecord, a as ContactMapStreamPhase, b as ContactMapSearchOutletContext, c as ContactMapOutletContext, d as ContactMapLogger, S as SerializedContactStreamRow } from "./wire-
|
|
4
|
+
import { C as ContactRecord, a as ContactMapStreamPhase, b as ContactMapSearchOutletContext, c as ContactMapOutletContext, d as ContactMapLogger, S as SerializedContactStreamRow } from "./wire-CVZNwmQx.mjs";
|
|
5
5
|
import { DirectoryTreeLabels, DirectoryTreeVirtualScrollOptions } from "@aiquants/directory-tree";
|
|
6
6
|
import * as _aiquants_virtualscroll from "@aiquants/virtualscroll";
|
|
7
7
|
import { VirtualScrollLocale } from "@aiquants/virtualscroll";
|
package/dist/client.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react from "react";
|
|
2
2
|
import { CSSProperties, ReactNode, RefObject, PointerEvent, MouseEvent } from "react";
|
|
3
3
|
import { UseFuzzySearchOptions } from "@aiquants/fuzzy-search/react";
|
|
4
|
-
import { C as ContactRecord, a as ContactMapStreamPhase, b as ContactMapSearchOutletContext, c as ContactMapOutletContext, d as ContactMapLogger, S as SerializedContactStreamRow } from "./wire-
|
|
4
|
+
import { C as ContactRecord, a as ContactMapStreamPhase, b as ContactMapSearchOutletContext, c as ContactMapOutletContext, d as ContactMapLogger, S as SerializedContactStreamRow } from "./wire-CVZNwmQx.js";
|
|
5
5
|
import { DirectoryTreeLabels, DirectoryTreeVirtualScrollOptions } from "@aiquants/directory-tree";
|
|
6
6
|
import * as _aiquants_virtualscroll from "@aiquants/virtualscroll";
|
|
7
7
|
import { VirtualScrollLocale } from "@aiquants/virtualscroll";
|