@oimlsmart/platform-server 0.2.6 → 0.2.7

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,15 @@
1
+ -- The per-store journal high-water (the ServerStore seam's
2
+ -- latestChangeSeqFor(store), the 2026-09-07 performance audit's G1):
3
+ -- the seam's DOC-ONLY contract since 0.2.2 ("the (store, seq) walk —
4
+ -- the index rides the same commit") lands. The read answers MAX(seq)
5
+ -- over one store's slice of the ONE global journal — the seq stays
6
+ -- global and monotone, this is a projection, never a second sequence.
7
+ -- Without the index the projection scans the journal's rowid walk and
8
+ -- filters; with it the read is a single indexed probe, which is the
9
+ -- contract's whole point (per-store conditional revalidation / SSE
10
+ -- resume probes paying one read, never a payload fetch).
11
+ --
12
+ -- CREATE INDEX IF NOT EXISTS is the idempotent guard (the migration
13
+ -- contract's expand-only discipline; a consumer that already carries
14
+ -- the index converges). schema.sql's mirror lands in the same commit.
15
+ CREATE INDEX IF NOT EXISTS idx_entity_changes_store_seq ON entity_changes (store, seq);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oimlsmart/platform-server",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "The OIML SMART platform server kernel: the store seam (ServerStore + the D1 and SQLite implementations), the canonical D1 migration set both deployments apply, the instance profile, the mailer, the RBAC map, the OIDC/OAuth client cones, and the shared role/permission vocabulary. Consumed by the smart monorepo (browser/) and the identity service.",
5
5
  "type": "module",
6
6
  "repository": {
package/src/store/d1.ts CHANGED
@@ -3480,6 +3480,16 @@ export class D1ServerStore implements ServerStore {
3480
3480
  return row?.seq ?? 0
3481
3481
  }
3482
3482
 
3483
+ async latestChangeSeqFor(store: string): Promise<number> {
3484
+ // One indexed probe over idx_entity_changes_store_seq (migration
3485
+ // 0026) — the per-store projection of the one journal; 0 when the
3486
+ // store carries no rows (MAX answers NULL on the empty set).
3487
+ const row = await this.stmt(
3488
+ 'SELECT MAX(seq) AS seq FROM entity_changes WHERE store = ?', store,
3489
+ ).first<{ seq: number | null }>()
3490
+ return row?.seq ?? 0
3491
+ }
3492
+
3483
3493
  // ── the platform event store (TODO.notify/01) ─────────────────────
3484
3494
  // The event rows port directly (D1 is SQLite) — the same statements
3485
3495
  // as sqlite/events.ts's sync half.
@@ -74,3 +74,14 @@ export function latestChangeSeq(): number {
74
74
  const row = getDb().prepare('SELECT MAX(seq) AS seq FROM entity_changes').get() as { seq: number | null }
75
75
  return row.seq ?? 0
76
76
  }
77
+
78
+ /** The per-store projection of the one journal (the seam's contract):
79
+ * MAX(seq) over the store's own slice, 0 on a store with no writes.
80
+ * Walks idx_entity_changes_store_seq — one indexed probe, never a
81
+ * journal scan. */
82
+ export function latestChangeSeqFor(store: string): number {
83
+ const row = getDb()
84
+ .prepare('SELECT MAX(seq) AS seq FROM entity_changes WHERE store = ?')
85
+ .get(store) as { seq: number | null }
86
+ return row.seq ?? 0
87
+ }
@@ -132,6 +132,12 @@ CREATE TABLE IF NOT EXISTS entity_changes (
132
132
  id TEXT NOT NULL,
133
133
  at TEXT NOT NULL DEFAULT (datetime('now'))
134
134
  );
135
+ -- The per-store high-water's walk (migration 0026, the seam's
136
+ -- latestChangeSeqFor(store)): MAX(seq) over one store's slice of the
137
+ -- one global journal costs a single indexed probe, never a journal
138
+ -- scan. The seq stays global and monotone — a projection, never a
139
+ -- second sequence.
140
+ CREATE INDEX IF NOT EXISTS idx_entity_changes_store_seq ON entity_changes (store, seq);
135
141
 
136
142
  -- TODO.notify/01 — the platform event store (the notification system's
137
143
  -- source of truth): one row per DECLARED notifiable act (the catalog,
@@ -84,6 +84,7 @@ import {
84
84
  deleteEntity,
85
85
  getEntity,
86
86
  latestChangeSeq,
87
+ latestChangeSeqFor,
87
88
  listEntities,
88
89
  putEntity,
89
90
  } from './sqlite/entities'
@@ -1022,6 +1023,9 @@ export function createSqliteServerStore(): ServerStore {
1022
1023
  async latestChangeSeq(): Promise<number> {
1023
1024
  return latestChangeSeq()
1024
1025
  },
1026
+ async latestChangeSeqFor(store: string): Promise<number> {
1027
+ return latestChangeSeqFor(store)
1028
+ },
1025
1029
 
1026
1030
  // ── the platform event store (TODO.notify/01) ──
1027
1031
  async appendEvent(input: {
package/src/store.ts CHANGED
@@ -2353,20 +2353,20 @@ export interface ServerStore {
2353
2353
  changesAfter(seq: number, limit?: number): Promise<EntityChange[]>
2354
2354
  /** The GLOBAL journal high-water: the bootstrap snapshot's ETag leg
2355
2355
  * (any write anywhere bumps the one seq — conservative, never
2356
- * stale).
2357
- *
2358
- * NAMED FOLLOW-UP, DOC ONLY (the smart side references it from
2359
- * server/routes/bootstrap.ts: the per-store `seqs` snapshot carries
2360
- * the journal position a delta-loading client would resume from):
2361
- * the PER-STORE high-water — `latestChangeSeqFor(store)` — answering
2362
- * MAX(seq) over one store's slice of the SAME journal. The contract
2363
- * when it lands: the seq stays global and monotone (the per-store
2364
- * read is a PROJECTION of the one journal, never a second
2365
- * sequence); it exists so a per-store conditional revalidation /
2366
- * SSE resume probe costs one indexed read instead of a payload
2367
- * fetch; and its query wants the (store, seq) walk — the index
2368
- * rides the same commit, expand-only per the migration contract. */
2356
+ * stale). */
2369
2357
  latestChangeSeq(): Promise<number>
2358
+ /** The PER-STORE high-water: MAX(seq) over one store's slice of the
2359
+ * SAME journal. The seq stays global and monotone — the per-store
2360
+ * read is a PROJECTION of the one journal, never a second sequence.
2361
+ * A store with no writes answers 0 (the empty journal's floor, the
2362
+ * same as the global form's). The read is one indexed probe over
2363
+ * idx_entity_changes_store_seq (migration 0026 — the (store, seq)
2364
+ * walk rode that commit, expand-only per the migration contract),
2365
+ * so a per-store conditional revalidation / SSE resume probe never
2366
+ * pays a journal scan. Landed for the 2026-09-07 performance
2367
+ * audit's G1: the smart side's bootstrap composite ETag switches
2368
+ * from the global seq to the per-set composite of these. */
2369
+ latestChangeSeqFor(store: string): Promise<number>
2370
2370
 
2371
2371
  // ── the platform event store (TODO.notify/01) ──
2372
2372
  /** Append one declared event (the emitter's write; one row inside the