@stone-js/store 0.8.15 → 0.8.17

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,49 @@
1
+ import { ClassType } from '@stone-js/core';
2
+ /** Options for the `@FeatureStore` declaration. */
3
+ export interface FeatureStoreOptions {
4
+ /**
5
+ * Whether the server keeps one instance per request. Default `true`, deliberately: a shared store
6
+ * leaks one visitor's state into the next visitor's page during server rendering, and the leak is
7
+ * invisible in development where there is only ever one request at a time.
8
+ */
9
+ perRequest?: boolean;
10
+ }
11
+ /**
12
+ * Declare a class as a feature's store.
13
+ *
14
+ * A feature owns its client, its service and its state, and this is how the state half is declared:
15
+ * a class extending `StateStore`, holding the actions that move it. Two statements in one, the way
16
+ * every declaration decorator works here:
17
+ *
18
+ * 1. **The container builds it.** Its constructor is auto-wired like any other class, which is what a
19
+ * data definition cannot express: the store's actions call the services they were handed.
20
+ * 2. **It activates the module.** The blueprint comes with the decorator, so declaring a store is the
21
+ * whole setup, and it is resolved under `store.<name>`, hydrated from the snapshot, and given the
22
+ * per-request lifetime, exactly like a store declared as data.
23
+ *
24
+ * The imperative counterpart is `defineStore(CompetitionStore, { name: 'competition' })`, and the two
25
+ * declare the same thing: neither paradigm can do something the other cannot.
26
+ *
27
+ * @param name - The name it is resolved under, in the container and in the snapshot. Defaults to the
28
+ * class name.
29
+ * @param options - The lifetime.
30
+ * @returns A class decorator.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * @FeatureStore('competition')
35
+ * export class CompetitionStore extends StateStore<CompetitionState> {
36
+ * private readonly client: CompetitionClient
37
+ *
38
+ * constructor ({ competitionClient }: { competitionClient: CompetitionClient }) {
39
+ * super({ list: [], selected: undefined })
40
+ * this.client = competitionClient
41
+ * }
42
+ *
43
+ * async load (): Promise<void> {
44
+ * this.setState({ list: await this.client.list() })
45
+ * }
46
+ * }
47
+ * ```
48
+ */
49
+ export declare const FeatureStore: <T extends ClassType = ClassType>(name?: string, options?: FeatureStoreOptions) => ClassDecorator;
@@ -1,11 +1,11 @@
1
- import { StoreDefinition } from '../defineStore.js';
1
+ import { StoreRegistration } from '../defineStore.js';
2
2
  import { ClassType } from '@stone-js/core';
3
3
  /**
4
4
  * Options for the `@Store` decorator: the stores to declare, and the `stone.store` bucket.
5
5
  */
6
6
  export interface StoreDecoratorOptions {
7
7
  /** The stores this application declares. */
8
- stores?: StoreDefinition[];
8
+ stores?: StoreRegistration[];
9
9
  /** The snapshot key the hydrated states live under. */
10
10
  snapshotKey?: string;
11
11
  }
@@ -18,25 +18,92 @@ export interface StoreDefinition<State extends Record<string, any> = Record<stri
18
18
  */
19
19
  perRequest?: boolean;
20
20
  }
21
+ /**
22
+ * A store written as a class: a feature's state and the actions that move it, together.
23
+ *
24
+ * The container builds it, so its constructor is auto-wired like any other class. That is what a data
25
+ * definition cannot express and the reason the class form exists: a `competition` module has its
26
+ * client, its service and its store, and the store's actions call the client.
27
+ */
28
+ export type StoreClass<State extends Record<string, any> = Record<string, any>> = new (...args: any[]) => IStore<State>;
29
+ /** A store built by a factory. It receives the container, and returns the store. */
30
+ export type StoreFactory<State extends Record<string, any> = Record<string, any>> = (container: any) => IStore<State>;
31
+ /**
32
+ * A registered store, in any of the three forms the framework accepts everywhere else.
33
+ *
34
+ * The data definition plays the plain role: state with no behaviour. The class and the factory carry
35
+ * behaviour, and both are built through the container, so a store can hold the services its actions
36
+ * need. `stone.store.stores` accepts any mix of the three.
37
+ */
38
+ export interface MetaStore<State extends Record<string, any> = Record<string, any>> {
39
+ /** The name it is resolved under, in the container and in the snapshot. */
40
+ name: string;
41
+ /** The class or the factory to build. */
42
+ module: StoreClass<State> | StoreFactory<State>;
43
+ /** Whether `module` is a class to construct. */
44
+ isClass?: boolean;
45
+ /** Whether `module` is a factory to call with the container. */
46
+ isFactory?: boolean;
47
+ /** Same semantics as {@link StoreDefinition.perRequest}, same default. */
48
+ perRequest?: boolean;
49
+ }
50
+ /** Anything `stone.store.stores` accepts. */
51
+ export type StoreRegistration<State extends Record<string, any> = Record<string, any>> = StoreDefinition<State> | MetaStore<State>;
52
+ /** Options for the class and factory forms of {@link defineStore}. */
53
+ export interface DefineStoreOptions {
54
+ /** The name it is resolved under. Defaults to the class name for a class; required for a factory. */
55
+ name?: string;
56
+ /** Whether the module is a factory rather than a class. */
57
+ isFactory?: boolean;
58
+ /** Same semantics as {@link StoreDefinition.perRequest}, same default. */
59
+ perRequest?: boolean;
60
+ }
21
61
  /**
22
62
  * Declare a store, imperatively.
23
63
  *
24
- * The counterpart of the `@Store()` decorator: same declaration, same registration, no decorators
25
- * required. Both end up in `stone.store.stores`, which is what the provider registers.
64
+ * Three forms, mirroring the rest of the framework. A data definition for state with no behaviour, a
65
+ * class for a feature whose store carries actions, and a factory for full control over construction.
66
+ * The class and factory forms are the imperative counterpart of `@FeatureStore()`: same declaration,
67
+ * same registration, no decorators required. Everything ends up in `stone.store.stores`, which is
68
+ * what the provider registers.
26
69
  *
27
- * @param definition - What the store declares.
28
- * @returns The definition, ready to be handed to the blueprint.
70
+ * @param definition - A data definition, a store class, or a store factory.
71
+ * @param options - The name and lifetime, for the class and factory forms.
72
+ * @returns The registration, ready to be handed to the blueprint.
29
73
  *
30
74
  * @example
31
75
  * ```typescript
76
+ * // Data: state with no behaviour.
32
77
  * export const tasksStore = defineStore({ name: 'tasks', state: { items: [], filter: 'all' } })
78
+ *
79
+ * // Class: the container builds it, so its actions can use injected services.
80
+ * export const competitionStore = defineStore(CompetitionStore, { name: 'competition' })
81
+ *
82
+ * // Factory: full control, the container in hand.
83
+ * export const liveStore = defineStore(
84
+ * (container) => StateStore.create({ scores: container.make('feed').initial() }),
85
+ * { name: 'live', isFactory: true }
86
+ * )
33
87
  * ```
34
88
  */
35
89
  export declare function defineStore<State extends Record<string, any>>(definition: StoreDefinition<State>): StoreDefinition<State>;
90
+ export declare function defineStore<State extends Record<string, any>>(definition: StoreClass<State> | StoreFactory<State>, options: DefineStoreOptions): MetaStore<State>;
36
91
  /**
37
- * Build a store from its definition.
92
+ * Whether a registration carries a module to build, rather than plain state.
93
+ *
94
+ * @param registration - The registration.
95
+ * @returns True for the class and factory forms.
96
+ */
97
+ export declare function isMetaStore<State extends Record<string, any>>(registration: StoreRegistration<State>): registration is MetaStore<State>;
98
+ /**
99
+ * Build a store from its registration, whichever form it was declared in.
100
+ *
101
+ * A class is constructed with the container, which is what auto-wires the services its actions use; a
102
+ * factory is called with it; a data definition needs nothing. The container is only required by the
103
+ * forms that use it, so building a plain store stays dependency-free.
38
104
  *
39
- * @param definition - What the store declares.
105
+ * @param registration - The registration.
106
+ * @param container - The container, for the class and factory forms.
40
107
  * @returns A new store.
41
108
  */
42
- export declare function makeStore<State extends Record<string, any>>(definition: StoreDefinition<State>): IStore<State>;
109
+ export declare function makeStore<State extends Record<string, any>>(registration: StoreRegistration<State>, container?: any): IStore<State>;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './declarations.js';
2
+ export * from './decorators/FeatureStore.js';
2
3
  export * from './decorators/Store.js';
3
4
  export * from './defineStore.js';
4
5
  export * from './options/StoreBlueprint.js';
package/dist/index.js CHANGED
@@ -158,31 +158,49 @@ class StateStore {
158
158
  }
159
159
  }
160
160
 
161
+ function defineStore(definition, options = {}) {
162
+ if (typeof definition !== 'function') {
163
+ return definition;
164
+ }
165
+ const name = options.name ?? definition.name;
166
+ if (name === undefined || name === '') {
167
+ throw new TypeError('A class or factory store needs a name: pass `{ name }`, since an anonymous function has none to fall back on.');
168
+ }
169
+ return {
170
+ name,
171
+ module: definition,
172
+ isClass: options.isFactory !== true,
173
+ isFactory: options.isFactory === true,
174
+ perRequest: options.perRequest
175
+ };
176
+ }
161
177
  /**
162
- * Declare a store, imperatively.
163
- *
164
- * The counterpart of the `@Store()` decorator: same declaration, same registration, no decorators
165
- * required. Both end up in `stone.store.stores`, which is what the provider registers.
178
+ * Whether a registration carries a module to build, rather than plain state.
166
179
  *
167
- * @param definition - What the store declares.
168
- * @returns The definition, ready to be handed to the blueprint.
169
- *
170
- * @example
171
- * ```typescript
172
- * export const tasksStore = defineStore({ name: 'tasks', state: { items: [], filter: 'all' } })
173
- * ```
180
+ * @param registration - The registration.
181
+ * @returns True for the class and factory forms.
174
182
  */
175
- function defineStore(definition) {
176
- return definition;
183
+ function isMetaStore(registration) {
184
+ return typeof registration.module === 'function';
177
185
  }
178
186
  /**
179
- * Build a store from its definition.
187
+ * Build a store from its registration, whichever form it was declared in.
180
188
  *
181
- * @param definition - What the store declares.
189
+ * A class is constructed with the container, which is what auto-wires the services its actions use; a
190
+ * factory is called with it; a data definition needs nothing. The container is only required by the
191
+ * forms that use it, so building a plain store stays dependency-free.
192
+ *
193
+ * @param registration - The registration.
194
+ * @param container - The container, for the class and factory forms.
182
195
  * @returns A new store.
183
196
  */
184
- function makeStore(definition) {
185
- return StateStore.create(definition.state);
197
+ function makeStore(registration, container) {
198
+ if (!isMetaStore(registration)) {
199
+ return StateStore.create(registration.state);
200
+ }
201
+ return registration.isFactory === true
202
+ ? registration.module(container)
203
+ : new registration.module(container);
186
204
  }
187
205
 
188
206
  /**
@@ -228,7 +246,9 @@ class StoreServiceProvider {
228
246
  const hydrated = this.hydratedStates(options.snapshotKey ?? 'stores');
229
247
  for (const definition of options.stores ?? []) {
230
248
  const build = () => {
231
- const store = makeStore(definition);
249
+ // The container is what auto-wires a class store's constructor and what a factory receives;
250
+ // a plain data definition ignores it.
251
+ const store = makeStore(definition, this.container);
232
252
  const state = hydrated[definition.name];
233
253
  // Adopted before anything can read the store, so the first render already has real state.
234
254
  if (state !== undefined) {
@@ -295,6 +315,61 @@ const storeBlueprint = {
295
315
  }
296
316
  };
297
317
 
318
+ /**
319
+ * Declare a class as a feature's store.
320
+ *
321
+ * A feature owns its client, its service and its state, and this is how the state half is declared:
322
+ * a class extending `StateStore`, holding the actions that move it. Two statements in one, the way
323
+ * every declaration decorator works here:
324
+ *
325
+ * 1. **The container builds it.** Its constructor is auto-wired like any other class, which is what a
326
+ * data definition cannot express: the store's actions call the services they were handed.
327
+ * 2. **It activates the module.** The blueprint comes with the decorator, so declaring a store is the
328
+ * whole setup, and it is resolved under `store.<name>`, hydrated from the snapshot, and given the
329
+ * per-request lifetime, exactly like a store declared as data.
330
+ *
331
+ * The imperative counterpart is `defineStore(CompetitionStore, { name: 'competition' })`, and the two
332
+ * declare the same thing: neither paradigm can do something the other cannot.
333
+ *
334
+ * @param name - The name it is resolved under, in the container and in the snapshot. Defaults to the
335
+ * class name.
336
+ * @param options - The lifetime.
337
+ * @returns A class decorator.
338
+ *
339
+ * @example
340
+ * ```ts
341
+ * @FeatureStore('competition')
342
+ * export class CompetitionStore extends StateStore<CompetitionState> {
343
+ * private readonly client: CompetitionClient
344
+ *
345
+ * constructor ({ competitionClient }: { competitionClient: CompetitionClient }) {
346
+ * super({ list: [], selected: undefined })
347
+ * this.client = competitionClient
348
+ * }
349
+ *
350
+ * async load (): Promise<void> {
351
+ * this.setState({ list: await this.client.list() })
352
+ * }
353
+ * }
354
+ * ```
355
+ */
356
+ const FeatureStore = (name, options = {}) => {
357
+ return classDecoratorLegacyWrapper((target, context) => {
358
+ addBlueprint(target, context, storeBlueprint, {
359
+ stone: {
360
+ store: {
361
+ stores: [{
362
+ name: name ?? target.name,
363
+ module: target,
364
+ isClass: true,
365
+ perRequest: options.perRequest
366
+ }]
367
+ }
368
+ }
369
+ });
370
+ });
371
+ };
372
+
298
373
  /**
299
374
  * Class decorator: give the application a store, declaratively.
300
375
  *
@@ -324,4 +399,4 @@ const Store = (options = {}) => {
324
399
  });
325
400
  };
326
401
 
327
- export { StateStore, Store, StoreServiceProvider, defineStore, makeStore, sameValue, storeAlias, storeBlueprint };
402
+ export { FeatureStore, StateStore, Store, StoreServiceProvider, defineStore, isMetaStore, makeStore, sameValue, storeAlias, storeBlueprint };
@@ -1,4 +1,4 @@
1
- import { StoreDefinition } from '../defineStore.js';
1
+ import { StoreRegistration } from '../defineStore.js';
2
2
  import { AppConfig, StoneBlueprint } from '@stone-js/core';
3
3
  /**
4
4
  * Store configuration bucket (`stone.store`).
@@ -9,7 +9,7 @@ export interface StoreConfig {
9
9
  *
10
10
  * Filled by `@Store()` and by handing `defineStore(...)` definitions here.
11
11
  */
12
- stores?: StoreDefinition[];
12
+ stores?: StoreRegistration[];
13
13
  /**
14
14
  * The snapshot key the hydrated states live under. Default `'stores'`.
15
15
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stone-js/store",
3
- "version": "0.8.15",
3
+ "version": "0.8.17",
4
4
  "description": "View-engine-agnostic universal store for Stone.js, with SSR hydration built in.",
5
5
  "author": "Mr. Stone <evensstone@gmail.com>",
6
6
  "license": "MIT",
@@ -54,7 +54,8 @@
54
54
  "typedoc": "^0.28.6",
55
55
  "typedoc-plugin-markdown": "^4.7.0",
56
56
  "typescript": "^5.6.3",
57
- "vitest": "^3.2.4"
57
+ "vitest": "^3.2.4",
58
+ "@stone-js/service-container": "0.8.17"
58
59
  },
59
60
  "ts-standard": {
60
61
  "globals": [
@@ -67,10 +68,10 @@
67
68
  ]
68
69
  },
69
70
  "dependencies": {
70
- "@stone-js/config": "0.8.15"
71
+ "@stone-js/config": "0.8.17"
71
72
  },
72
73
  "peerDependencies": {
73
- "@stone-js/core": "0.8.15"
74
+ "@stone-js/core": "0.8.17"
74
75
  },
75
76
  "scripts": {
76
77
  "lint": "ts-standard src",