@spfn/core 0.3.0-beta.25 → 0.3.0-beta.27
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 +4 -1
- package/dist/event/sse/client.d.ts +1 -1
- package/dist/event/sse/client.js +12 -1
- package/dist/event/sse/client.js.map +1 -1
- package/dist/event/sse/index.d.ts +2 -2
- package/dist/server/index.d.ts +1 -1
- package/dist/state/client.d.ts +144 -0
- package/dist/state/client.js +478 -0
- package/dist/state/client.js.map +1 -0
- package/dist/state/index.d.ts +74 -11
- package/dist/state/index.js +70 -12
- package/dist/state/index.js.map +1 -1
- package/dist/state/react.d.ts +34 -0
- package/dist/state/react.js +44 -0
- package/dist/state/react.js.map +1 -0
- package/dist/{types-DqXYtI69.d.ts → types-04gu3uMj.d.ts} +7 -0
- package/package.json +21 -2
- package/src/event/README.md +21 -0
- package/src/state/README.md +290 -27
package/dist/state/index.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ interface StateShape {
|
|
|
21
21
|
data: TSchema | undefined;
|
|
22
22
|
input: TSchema | undefined;
|
|
23
23
|
read: boolean;
|
|
24
|
+
/** Who the callbacks' `principal` is, from `.principal<P>()`; `unknown` until declared. */
|
|
25
|
+
principal: unknown;
|
|
24
26
|
}
|
|
25
27
|
/** The shape of `defineState(id)` before any call. */
|
|
26
28
|
interface EmptyStateShape extends StateShape {
|
|
@@ -30,6 +32,7 @@ interface EmptyStateShape extends StateShape {
|
|
|
30
32
|
data: undefined;
|
|
31
33
|
input: undefined;
|
|
32
34
|
read: false;
|
|
35
|
+
principal: unknown;
|
|
33
36
|
}
|
|
34
37
|
/** `T` with the fields of `U` replaced. */
|
|
35
38
|
type WithShape<T extends StateShape, U extends Partial<StateShape>> = {
|
|
@@ -57,22 +60,38 @@ type StateResult<T extends StateShape> = T['data'] extends TSchema ? {
|
|
|
57
60
|
type StateSnapshot<T extends StateShape> = Simplify<{
|
|
58
61
|
at: string;
|
|
59
62
|
} & StateResult<T>>;
|
|
60
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* What `.canRead()` decides on. `principal` may be `undefined`: at SSE connect
|
|
65
|
+
* it is what `resolvePrincipal` returned.
|
|
66
|
+
*/
|
|
61
67
|
type PermissionContext<T extends StateShape> = T['key'] extends TObject ? {
|
|
62
68
|
subject: string;
|
|
63
|
-
principal:
|
|
69
|
+
principal: T['principal'] | undefined;
|
|
64
70
|
key: Static<T['key']>;
|
|
65
71
|
} : {
|
|
66
72
|
subject: string;
|
|
67
|
-
principal:
|
|
73
|
+
principal: T['principal'] | undefined;
|
|
68
74
|
};
|
|
69
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* What `.canWrite()` decides on. Only the write route asks it, and with
|
|
77
|
+
* `.principal()` declared that route refuses a request whose `auth` is
|
|
78
|
+
* `undefined` or `null` (401).
|
|
79
|
+
*/
|
|
80
|
+
type WritePermissionContext<T extends StateShape> = Simplify<Omit<PermissionContext<T>, 'principal'> & {
|
|
81
|
+
principal: T['principal'];
|
|
82
|
+
}>;
|
|
83
|
+
/**
|
|
84
|
+
* What `read()` receives. A shared state's value does not depend on the
|
|
85
|
+
* subject, so it gets none. `principal` may be `undefined`: the change
|
|
86
|
+
* signal's re-read has no request.
|
|
87
|
+
*/
|
|
70
88
|
type ReadContext<T extends StateShape> = T['key'] extends TObject ? T['shared'] extends true ? {
|
|
71
|
-
principal:
|
|
89
|
+
principal: T['principal'] | undefined;
|
|
72
90
|
key: Static<T['key']>;
|
|
73
91
|
} : PermissionContext<T> : PermissionContext<T>;
|
|
74
|
-
/** What `write()` receives: the read context
|
|
75
|
-
type WriteContext<T extends StateShape, I extends TSchema> = Simplify<ReadContext<T> & {
|
|
92
|
+
/** What `write()` receives: the read context, the write route's principal and the validated input. */
|
|
93
|
+
type WriteContext<T extends StateShape, I extends TSchema> = Simplify<Omit<ReadContext<T>, 'principal'> & {
|
|
94
|
+
principal: T['principal'];
|
|
76
95
|
input: Static<I>;
|
|
77
96
|
}>;
|
|
78
97
|
/** The payload of `state.changed.emit()`: exactly what names the value to re-read. */
|
|
@@ -137,6 +156,8 @@ interface StateDefinition {
|
|
|
137
156
|
id: string;
|
|
138
157
|
key?: TObject;
|
|
139
158
|
shared: boolean;
|
|
159
|
+
/** Whether `.principal()` was declared: the write route then refuses a request whose `auth` is `undefined` or `null`. */
|
|
160
|
+
principal: boolean;
|
|
140
161
|
value?: TSchema;
|
|
141
162
|
data?: TSchema;
|
|
142
163
|
middlewares: (MiddlewareHandler | NamedMiddleware<string>)[];
|
|
@@ -204,6 +225,15 @@ declare class StateBuilder<TId extends string, T extends StateShape> {
|
|
|
204
225
|
data<D extends TSchema>(schema: D): StateBuilder<TId, WithShape<T, {
|
|
205
226
|
data: D;
|
|
206
227
|
}>>;
|
|
228
|
+
/**
|
|
229
|
+
* Declare who the callbacks' `principal` is — what the app's auth
|
|
230
|
+
* middleware sets as `c.get('auth')`. Type-only, but for one rule: the write
|
|
231
|
+
* route refuses a request without a principal (401), so `.canWrite()` and
|
|
232
|
+
* `.write()` receive `P`; `.read()` and `.canRead()` receive `P | undefined`.
|
|
233
|
+
*/
|
|
234
|
+
principal<P>(): StateBuilder<TId, WithShape<T, {
|
|
235
|
+
principal: P;
|
|
236
|
+
}>>;
|
|
207
237
|
/**
|
|
208
238
|
* Middleware for the two per-state routes, `get<Id>State` / `put<Id>State`.
|
|
209
239
|
* Not authorization: the snapshot and the SSE stream cannot run it, so a
|
|
@@ -217,7 +247,7 @@ declare class StateBuilder<TId extends string, T extends StateShape> {
|
|
|
217
247
|
/** Who may read. Required on a keyed state; checked on HTTP and at SSE connect. */
|
|
218
248
|
canRead(predicate: (ctx: PermissionContext<T>) => boolean | Promise<boolean>): StateBuilder<TId, T>;
|
|
219
249
|
/** Who may write. Absent → `canRead` decides. */
|
|
220
|
-
canWrite(predicate: (ctx:
|
|
250
|
+
canWrite(predicate: (ctx: WritePermissionContext<T>) => boolean | Promise<boolean>): StateBuilder<TId, T>;
|
|
221
251
|
/** The only source of a value. Required. */
|
|
222
252
|
read(fn: (ctx: ReadContext<T>) => StateResult<T> | Promise<StateResult<T>>): StateBuilder<TId, WithShape<T, {
|
|
223
253
|
read: true;
|
|
@@ -294,6 +324,24 @@ declare function defineState<TId extends string>(id: TId): StateBuilder<TId, Emp
|
|
|
294
324
|
|
|
295
325
|
/** Who a request is for. `null` answers 401. */
|
|
296
326
|
type SubjectResolver = (c: Context) => string | null;
|
|
327
|
+
/**
|
|
328
|
+
* What the router gives the snapshot route `GET <path>` alone. A per-state
|
|
329
|
+
* route keeps its own state's `.use()` / `.skip()` / `.contract()`, and the
|
|
330
|
+
* SSE stream runs none of them.
|
|
331
|
+
*/
|
|
332
|
+
interface SnapshotRouteOptions {
|
|
333
|
+
/** Middleware for the snapshot route only. Not authorization: the stream never runs it (see README). */
|
|
334
|
+
use?: (MiddlewareHandler | NamedMiddleware<string>)[];
|
|
335
|
+
/** Server-level named middlewares the snapshot route skips; `'*'` skips every one. */
|
|
336
|
+
skip?: string[] | '*';
|
|
337
|
+
/** Publishes the snapshot route in the contract; the response schema is derived. */
|
|
338
|
+
contract?: StateContract;
|
|
339
|
+
}
|
|
340
|
+
/** What the browser cannot read from a type: every state, in declaration order, its key fields and whether it has a write. */
|
|
341
|
+
type StatesManifest = Record<string, {
|
|
342
|
+
key: string[];
|
|
343
|
+
write: boolean;
|
|
344
|
+
}>;
|
|
297
345
|
|
|
298
346
|
/**
|
|
299
347
|
* State Router
|
|
@@ -303,7 +351,12 @@ type SubjectResolver = (c: Context) => string | null;
|
|
|
303
351
|
* subscribes each state's change signal — once, here.
|
|
304
352
|
*/
|
|
305
353
|
|
|
306
|
-
|
|
354
|
+
/**
|
|
355
|
+
* Where the router mounts, whom a request is for, and the snapshot route's own
|
|
356
|
+
* `use` / `skip` / `contract` (`SnapshotRouteOptions`) — those reach `GET <path>`
|
|
357
|
+
* only, never a per-state route or the SSE stream.
|
|
358
|
+
*/
|
|
359
|
+
interface StateRouterOptions extends SnapshotRouteOptions {
|
|
307
360
|
/**
|
|
308
361
|
* Where the routes are mounted: `GET <path>`, `GET|PUT <path>/<id>`.
|
|
309
362
|
* @default '/state'
|
|
@@ -328,12 +381,22 @@ type WriteInput<S extends RoutableState> = KeyQuery<S> & {
|
|
|
328
381
|
type WriteRoutes<S extends Record<string, RoutableState>> = {
|
|
329
382
|
[K in keyof S as Shape<S[K]>['input'] extends TSchema ? `put${Capitalize<S[K]['id']>}State` : never]: RouteDef<WriteInput<S[K]>, {}, StateSnapshot<Shape<S[K]>>>;
|
|
330
383
|
};
|
|
331
|
-
/**
|
|
384
|
+
/**
|
|
385
|
+
* What `GET <path>` answers: every state the subject may read and whose key
|
|
386
|
+
* the query carries, and the manifest of every state — its key fields and
|
|
387
|
+
* whether it has a write — in declaration order.
|
|
388
|
+
*/
|
|
332
389
|
type StatesSnapshot<S extends Record<string, RoutableState>> = {
|
|
333
390
|
at: string;
|
|
334
391
|
states: {
|
|
335
392
|
[K in keyof S as S[K]['id']]?: StateResult<Shape<S[K]>>;
|
|
336
393
|
};
|
|
394
|
+
manifest: {
|
|
395
|
+
[K in keyof S as S[K]['id']]: {
|
|
396
|
+
key: string[];
|
|
397
|
+
write: boolean;
|
|
398
|
+
};
|
|
399
|
+
};
|
|
337
400
|
};
|
|
338
401
|
/** The routes one state router derives, typed for `createApi` and the route map. */
|
|
339
402
|
type StateRoutes<S extends Record<string, RoutableState>> = ReadRoutes<S> & WriteRoutes<S> & {
|
|
@@ -377,4 +440,4 @@ type InferStateInput<R extends StateRouter<any>, TId extends string> = Shape<Sta
|
|
|
377
440
|
*/
|
|
378
441
|
declare function defineStateRouter<S extends Record<string, RoutableState>>(states: S, options?: StateRouterOptions): StateRouter<S>;
|
|
379
442
|
|
|
380
|
-
export { type ChangedPayload, type GrantTarget, type InferStateInput, type InferStateKey, type InferStateValue, type PermissionContext, type ReadContext, type RevokeTarget, type RoutableState, StateBuilder, type StateContract, type StateEvents, type StateFrame, type StateKey, type StateResult, type StateRouter, type StateRouterOptions, type StateRoutes, type StateShape, type StateSnapshot, type StateValue, type StateWriteOptions, type StatesSnapshot, type SubjectResolver, type WriteContext, defineState, defineStateRouter };
|
|
443
|
+
export { type ChangedPayload, type GrantTarget, type InferStateInput, type InferStateKey, type InferStateValue, type PermissionContext, type ReadContext, type RevokeTarget, type RoutableState, type SnapshotRouteOptions, StateBuilder, type StateContract, type StateEvents, type StateFrame, type StateKey, type StateResult, type StateRouter, type StateRouterOptions, type StateRoutes, type StateShape, type StateSnapshot, type StateValue, type StateWriteOptions, type StatesManifest, type StatesSnapshot, type SubjectResolver, type WriteContext, type WritePermissionContext, defineState, defineStateRouter };
|
package/dist/state/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Type, Kind } from '@sinclair/typebox';
|
|
2
2
|
import { Value } from '@sinclair/typebox/value';
|
|
3
|
+
import { getTransactionContext, onAfterCommit } from '@spfn/core/db';
|
|
3
4
|
import { defineEvent } from '@spfn/core/event';
|
|
4
5
|
import { logger } from '@spfn/core/logger';
|
|
5
6
|
import { defineRouter, route } from '@spfn/core/route';
|
|
@@ -83,7 +84,12 @@ function toFrame(def, snapshot, subject, key) {
|
|
|
83
84
|
}
|
|
84
85
|
async function readAndPublish(def, event, subject, key, principal) {
|
|
85
86
|
const snapshot = await readSnapshot(def, readContext(def, subject, principal, key));
|
|
86
|
-
|
|
87
|
+
const frame = toFrame(def, snapshot, subject, key);
|
|
88
|
+
if (getTransactionContext()) {
|
|
89
|
+
onAfterCommit(() => event.emit(frame));
|
|
90
|
+
} else {
|
|
91
|
+
await event.emit(frame);
|
|
92
|
+
}
|
|
87
93
|
return snapshot;
|
|
88
94
|
}
|
|
89
95
|
function framePayloadSchema(def) {
|
|
@@ -154,7 +160,7 @@ async function onSignal(state, payload) {
|
|
|
154
160
|
try {
|
|
155
161
|
await readAndPublish(state._definition, state._event, subject, key, void 0);
|
|
156
162
|
} catch (error) {
|
|
157
|
-
stateLogger.warn(`State "${state.id}" could not be re-read on its change signal; no frame was sent`, {
|
|
163
|
+
stateLogger.warn(`State "${state.id}" could not be re-read or published on its change signal; no frame was sent`, {
|
|
158
164
|
state: state.id,
|
|
159
165
|
error: error instanceof Error ? error.message : String(error)
|
|
160
166
|
});
|
|
@@ -207,6 +213,16 @@ var StateBuilder = class _StateBuilder {
|
|
|
207
213
|
this.assertOnce("data", !!this._definition.data);
|
|
208
214
|
return this.with({ data: schema });
|
|
209
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* Declare who the callbacks' `principal` is — what the app's auth
|
|
218
|
+
* middleware sets as `c.get('auth')`. Type-only, but for one rule: the write
|
|
219
|
+
* route refuses a request without a principal (401), so `.canWrite()` and
|
|
220
|
+
* `.write()` receive `P`; `.read()` and `.canRead()` receive `P | undefined`.
|
|
221
|
+
*/
|
|
222
|
+
principal() {
|
|
223
|
+
this.assertOnce("principal", this._definition.principal);
|
|
224
|
+
return this.with({ principal: true });
|
|
225
|
+
}
|
|
210
226
|
/**
|
|
211
227
|
* Middleware for the two per-state routes, `get<Id>State` / `put<Id>State`.
|
|
212
228
|
* Not authorization: the snapshot and the SSE stream cannot run it, so a
|
|
@@ -371,7 +387,7 @@ function defineState(id) {
|
|
|
371
387
|
`defineState("${id}"): a state id must match ${STATE_ID}. It becomes part of the route names get<Id>State / put<Id>State and of the event name state.<id>.changed. Use a lowerCamel identifier, e.g. "myState".`
|
|
372
388
|
);
|
|
373
389
|
}
|
|
374
|
-
return new StateBuilder(id, { id, shared: false, middlewares: [] });
|
|
390
|
+
return new StateBuilder(id, { id, shared: false, principal: false, middlewares: [] });
|
|
375
391
|
}
|
|
376
392
|
var defaultSubject = (c) => c.get("auth")?.userId ?? null;
|
|
377
393
|
function stateRouteName(method, id) {
|
|
@@ -384,19 +400,30 @@ function requireSubject(c, resolveSubject) {
|
|
|
384
400
|
}
|
|
385
401
|
return subject;
|
|
386
402
|
}
|
|
403
|
+
function writePrincipal(def, c) {
|
|
404
|
+
const principal = c.get("auth");
|
|
405
|
+
if (def.principal && principal == null) {
|
|
406
|
+
throw new UnauthorizedError({
|
|
407
|
+
message: `State "${def.id}" declares .principal(), and this request carries none`
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
return principal;
|
|
411
|
+
}
|
|
387
412
|
async function assertPermitted(def, can, ctx) {
|
|
388
413
|
const predicate = can === "canWrite" ? def.canWrite ?? def.canRead : def.canRead;
|
|
389
414
|
if (predicate && !await predicate(ctx)) {
|
|
390
415
|
throw new ForbiddenError({ message: `${can === "canWrite" ? "Writing" : "Reading"} state "${def.id}" is not permitted` });
|
|
391
416
|
}
|
|
392
417
|
}
|
|
393
|
-
function
|
|
418
|
+
function resultSchema2(def) {
|
|
394
419
|
return Type.Object({
|
|
395
|
-
at: Type.String(),
|
|
396
420
|
value: def.value,
|
|
397
421
|
...def.data ? { data: def.data } : {}
|
|
398
422
|
});
|
|
399
423
|
}
|
|
424
|
+
function responseSchema(def) {
|
|
425
|
+
return Type.Object({ at: Type.String(), ...resultSchema2(def).properties });
|
|
426
|
+
}
|
|
400
427
|
function routeContract(def, write) {
|
|
401
428
|
if (!def.contract) {
|
|
402
429
|
return void 0;
|
|
@@ -431,7 +458,7 @@ function writeRoute(state, path, resolveSubject) {
|
|
|
431
458
|
const write = def.write;
|
|
432
459
|
return stateRoute(def, "put", `${path}/${def.id}`, { body: write.input, ...keyInput(def) }).handler(async (c) => {
|
|
433
460
|
const subject = requireSubject(c.raw, resolveSubject);
|
|
434
|
-
const principal = c.raw
|
|
461
|
+
const principal = writePrincipal(def, c.raw);
|
|
435
462
|
const { body, query } = await c.data();
|
|
436
463
|
const key = def.key ? pickKey(def.key, query) : void 0;
|
|
437
464
|
await assertPermitted(def, "canWrite", permissionContext(def, subject, principal, key));
|
|
@@ -459,8 +486,28 @@ async function snapshotEntry(def, subject, principal, query, at) {
|
|
|
459
486
|
const { value, data } = await readSnapshot(def, readContext(def, subject, principal, key), at);
|
|
460
487
|
return def.data ? { value, data } : { value };
|
|
461
488
|
}
|
|
462
|
-
function
|
|
463
|
-
return
|
|
489
|
+
function stateManifest(states) {
|
|
490
|
+
return Object.fromEntries(states.map((state) => [state.id, {
|
|
491
|
+
key: Object.keys(state._definition.key?.properties ?? {}),
|
|
492
|
+
write: state._definition.write !== void 0
|
|
493
|
+
}]));
|
|
494
|
+
}
|
|
495
|
+
function snapshotResponseSchema(states) {
|
|
496
|
+
const manifestEntry = Type.Object({ key: Type.Array(Type.String()), write: Type.Boolean() });
|
|
497
|
+
return Type.Object({
|
|
498
|
+
at: Type.String(),
|
|
499
|
+
states: Type.Object(Object.fromEntries(states.map((state) => [state.id, Type.Optional(resultSchema2(state._definition))]))),
|
|
500
|
+
manifest: Type.Object(Object.fromEntries(states.map((state) => [state.id, manifestEntry])))
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
function snapshotBuilder(states, path, options) {
|
|
504
|
+
const builder = route.get(path).input({ query: snapshotQuery(states) });
|
|
505
|
+
const used = options.use?.length ? builder.use(options.use) : builder;
|
|
506
|
+
const skipped = options.skip ? used.skip(options.skip) : used;
|
|
507
|
+
return options.contract ? skipped.contract(stripUndefined({ ...options.contract, response: snapshotResponseSchema(states) })) : skipped;
|
|
508
|
+
}
|
|
509
|
+
function snapshotRoute(states, manifest, path, resolveSubject, options = {}) {
|
|
510
|
+
return snapshotBuilder(states, path, options).handler(async (c) => {
|
|
464
511
|
const subject = requireSubject(c.raw, resolveSubject);
|
|
465
512
|
const principal = c.raw.get("auth");
|
|
466
513
|
const { query } = await c.data();
|
|
@@ -468,7 +515,8 @@ function snapshotRoute(states, path, resolveSubject) {
|
|
|
468
515
|
const entries = await Promise.all(states.map((state) => snapshotEntry(state._definition, subject, principal, query, at)));
|
|
469
516
|
return {
|
|
470
517
|
at,
|
|
471
|
-
states: Object.fromEntries(states.map((state, index) => [state.id, entries[index]]).filter(([, entry]) => entry !== void 0))
|
|
518
|
+
states: Object.fromEntries(states.map((state, index) => [state.id, entries[index]]).filter(([, entry]) => entry !== void 0)),
|
|
519
|
+
manifest
|
|
472
520
|
};
|
|
473
521
|
});
|
|
474
522
|
}
|
|
@@ -479,17 +527,19 @@ function defineStateRouter(states, options = {}) {
|
|
|
479
527
|
const list = Object.values(states);
|
|
480
528
|
assertPath(path);
|
|
481
529
|
list.forEach(assertRoutable);
|
|
530
|
+
assertSnapshotMiddlewareIsNotAuthorization(list, options);
|
|
482
531
|
assertDistinctIds(list);
|
|
483
532
|
list.forEach((state) => assertNotRouted(state, path));
|
|
484
533
|
list.forEach((state) => bindState(state._definition, path));
|
|
485
534
|
list.forEach(subscribeChangeSignals);
|
|
486
535
|
return {
|
|
487
|
-
router: defineRouter(deriveRoutes(list, path, options
|
|
536
|
+
router: defineRouter(deriveRoutes(list, path, options)),
|
|
488
537
|
events: Object.fromEntries(list.map((state) => [stateEventName(state.id), state._event])),
|
|
489
538
|
states
|
|
490
539
|
};
|
|
491
540
|
}
|
|
492
|
-
function deriveRoutes(states, path,
|
|
541
|
+
function deriveRoutes(states, path, options) {
|
|
542
|
+
const resolveSubject = options.subject ?? defaultSubject;
|
|
493
543
|
const routes = {};
|
|
494
544
|
for (const state of states) {
|
|
495
545
|
routes[stateRouteName("get", state.id)] = readRoute(state, path, resolveSubject);
|
|
@@ -497,7 +547,7 @@ function deriveRoutes(states, path, resolveSubject) {
|
|
|
497
547
|
routes[stateRouteName("put", state.id)] = writeRoute(state, path, resolveSubject);
|
|
498
548
|
}
|
|
499
549
|
}
|
|
500
|
-
routes.getStates = snapshotRoute(states, path, resolveSubject);
|
|
550
|
+
routes.getStates = snapshotRoute(states, stateManifest(states), path, resolveSubject, options);
|
|
501
551
|
return routes;
|
|
502
552
|
}
|
|
503
553
|
function assertPath(path) {
|
|
@@ -536,6 +586,14 @@ function assertMiddlewareIsNotAuthorization(state) {
|
|
|
536
586
|
);
|
|
537
587
|
}
|
|
538
588
|
}
|
|
589
|
+
function assertSnapshotMiddlewareIsNotAuthorization(states, options) {
|
|
590
|
+
const unguarded = states.find((state) => !state._definition.canRead);
|
|
591
|
+
if (((options.use?.length ?? 0) > 0 || options.skip) && unguarded) {
|
|
592
|
+
throw new Error(
|
|
593
|
+
`defineStateRouter: the router gives the snapshot route use or skip, but state "${unguarded.id}" declares no .canRead(). The snapshot's middleware never reaches the SSE stream, which serves the same state, so it cannot authorize. Authorization belongs in .canRead(); a state readable by every subject declares .canRead(() => true) to say so.`
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
539
597
|
function assertChangedBy(state, event) {
|
|
540
598
|
const payload = event.schema;
|
|
541
599
|
const signal = changedPayloadSchema(state._definition);
|