@statorjs/stator 2.10.0-next.1 → 2.11.0-next.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.
@@ -198,6 +198,7 @@ export function defineApiRoute<TReads extends ReadonlyArray<AnyMachineDef>>(
198
198
  __isStatorQueryRoute: true,
199
199
  reads: config.reads ? [...config.reads] : [],
200
200
  handler: config.handler,
201
+ revision: config.revision,
201
202
  }
202
203
  }
203
204
  return {
@@ -248,6 +249,7 @@ export interface QueryRouteDefinition {
248
249
  request: RouteRequest,
249
250
  helpers: QueryRouteHelpers,
250
251
  ) => Promise<QueryRouteResult> | QueryRouteResult
252
+ revision?: () => string | number | Promise<string | number>
251
253
  }
252
254
 
253
255
  export interface DefineQueryRouteConfig<TReads extends ReadonlyArray<AnyMachineDef> = readonly []> {
@@ -259,6 +261,12 @@ export interface DefineQueryRouteConfig<TReads extends ReadonlyArray<AnyMachineD
259
261
  request: RouteRequest,
260
262
  helpers: QueryRouteHelpers<TReads>,
261
263
  ) => Promise<QueryRouteResult> | QueryRouteResult
264
+ /** The revision ledger: a CHEAP fingerprint of everything the body depends
265
+ * on (e.g. `() => String(maxUpdatedAt())` — one indexed SELECT). When
266
+ * present, the ETag derives from it and a matching `If-None-Match`
267
+ * answers 304 WITHOUT invoking the handler — a polling crawler costs the
268
+ * fingerprint, not the render. Must change whenever the body could. */
269
+ revision?: () => string | number | Promise<string | number>
262
270
  }
263
271
 
264
272
  export function isStatorQueryRoute(v: unknown): v is QueryRouteDefinition {
@@ -23,9 +23,18 @@ export interface SessionState {
23
23
  claimsDirty: boolean
24
24
  }
25
25
 
26
+ /** Layer-3 provability tracking: did this request USE the session plane
27
+ * (establish/dispatch/session-read) or READ claims? Either makes the
28
+ * response unprovable as anonymous-identical, so no derived Cache-Control. */
29
+ export interface SessionUse {
30
+ used: boolean
31
+ claimsRead: boolean
32
+ }
33
+
26
34
  declare module 'hono' {
27
35
  interface ContextVariableMap {
28
36
  statorSession: SessionState
37
+ statorSessionUse: SessionUse
29
38
  }
30
39
  }
31
40
 
@@ -34,6 +43,40 @@ export function getSessionState(c: Context): SessionState | undefined {
34
43
  return c.get('statorSession')
35
44
  }
36
45
 
46
+ /** The request's session-use flags (created on first access). */
47
+ export function sessionUse(c: Context): SessionUse {
48
+ let use = c.get('statorSessionUse')
49
+ if (!use) {
50
+ use = { used: false, claimsRead: false }
51
+ c.set('statorSessionUse', use)
52
+ }
53
+ return use
54
+ }
55
+
56
+ /** The session id this request WOULD have — established state or the arriving
57
+ * cookie — without creating anything. Null for a first-contact visitor. */
58
+ export function peekSessionId(c: Context): string | null {
59
+ return getSessionState(c)?.sid ?? getCookie(c, SESSION_COOKIE) ?? null
60
+ }
61
+
62
+ /** Resume an EXISTING session from the arriving cookie (populate the shared
63
+ * state, no cookie write) — or return null for first contact. Resuming is
64
+ * not establishment: laziness governs CREATION only. */
65
+ export function resumeSession(c: Context): SessionState | null {
66
+ const established = getSessionState(c)
67
+ if (established) return established
68
+ const cookieSid = getCookie(c, SESSION_COOKIE)
69
+ if (cookieSid === undefined) return null
70
+ const state: SessionState = {
71
+ sid: cookieSid,
72
+ isNew: false,
73
+ claims: undefined,
74
+ claimsDirty: false,
75
+ }
76
+ c.set('statorSession', state)
77
+ return state
78
+ }
79
+
37
80
  /** Secure cookie flag — set when running behind HTTPS. Enabled by
38
81
  * NODE_ENV=production; can be overridden via STATOR_SECURE_COOKIE. */
39
82
  function shouldUseSecureCookie(): boolean {
@@ -65,22 +108,26 @@ export function setSessionCookie(c: Context, sessionId: string): void {
65
108
  })
66
109
  }
67
110
 
111
+ /**
112
+ * ESTABLISH the session — the lazy-creation trigger (dispatches, session-
113
+ * machine reads, SSE connects, explicit session ops call this; anonymous
114
+ * page/data GETs never do). Idempotent per request: the first caller
115
+ * establishes, everyone after reads the same state. A first-contact visitor
116
+ * gets a fresh sid + Set-Cookie HERE and nowhere earlier — the whole
117
+ * cacheable-read-path layer 1.
118
+ */
68
119
  export function getOrCreateSessionId(c: Context): {
69
120
  sessionId: string
70
121
  isNew: boolean
71
122
  } {
72
- // Idempotent per request: the first caller (middleware or handler) establishes
73
- // the session; everyone after reads the same one. Closes the double-create bug.
74
- const established = c.get('statorSession')
75
- if (established) return { sessionId: established.sid, isNew: established.isNew }
76
-
77
- const cookieSid = getCookie(c, SESSION_COOKIE)
78
- const sessionId = cookieSid ?? randomUUID()
79
- const isNew = cookieSid === undefined
80
- if (isNew) setSessionCookie(c, sessionId)
123
+ sessionUse(c).used = true
124
+ const resumed = resumeSession(c)
125
+ if (resumed) return { sessionId: resumed.sid, isNew: resumed.isNew }
81
126
 
82
- c.set('statorSession', { sid: sessionId, isNew, claims: undefined, claimsDirty: false })
83
- return { sessionId, isNew }
127
+ const sessionId = randomUUID()
128
+ setSessionCookie(c, sessionId)
129
+ c.set('statorSession', { sid: sessionId, isNew: true, claims: undefined, claimsDirty: false })
130
+ return { sessionId, isNew: true }
84
131
  }
85
132
 
86
133
  /**
@@ -94,6 +94,11 @@ export interface ElementSpecificAttributes {
94
94
  autofocus?: boolean
95
95
  accept?: string
96
96
  multiple?: boolean
97
+ /** Associates this element with a `<form>` elsewhere in the document by
98
+ * id — the standard way to submit an element that can't live inside
99
+ * the form's own DOM subtree (e.g. a per-row control alongside a
100
+ * page-wide bulk-action form). `button` already carries this. */
101
+ form?: string
97
102
  }
98
103
  label: { for?: string }
99
104
  form: {
@@ -101,6 +106,11 @@ export interface ElementSpecificAttributes {
101
106
  method?: 'get' | 'post' | 'GET' | 'POST' | 'dialog'
102
107
  enctype?: 'application/x-www-form-urlencoded' | 'multipart/form-data' | 'text/plain'
103
108
  novalidate?: boolean
109
+ /** Native inline event-handler attribute — a literal JS source string
110
+ * (`"return confirm(...)"`), distinct from the `on:submit={...}`
111
+ * Stator directive. Useful for a plain, no-hydration `confirm()`
112
+ * guard that doesn't otherwise need an island. */
113
+ onsubmit?: string
104
114
  }
105
115
  select: {
106
116
  name?: string