@stone-js/store 0.8.14 → 0.8.16
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/dist/decorators/FeatureStore.d.ts +49 -0
- package/dist/decorators/Store.d.ts +2 -2
- package/dist/defineStore.d.ts +74 -7
- package/dist/index.d.ts +1 -0
- package/dist/index.js +94 -19
- package/dist/options/StoreBlueprint.d.ts +2 -2
- package/package.json +5 -4
|
@@ -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 {
|
|
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?:
|
|
8
|
+
stores?: StoreRegistration[];
|
|
9
9
|
/** The snapshot key the hydrated states live under. */
|
|
10
10
|
snapshotKey?: string;
|
|
11
11
|
}
|
package/dist/defineStore.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
25
|
-
*
|
|
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 -
|
|
28
|
-
* @
|
|
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
|
-
*
|
|
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
|
|
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>>(
|
|
109
|
+
export declare function makeStore<State extends Record<string, any>>(registration: StoreRegistration<State>, container?: any): IStore<State>;
|
package/dist/index.d.ts
CHANGED
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
|
-
*
|
|
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
|
|
168
|
-
* @returns
|
|
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
|
|
176
|
-
return
|
|
183
|
+
function isMetaStore(registration) {
|
|
184
|
+
return typeof registration.module === 'function';
|
|
177
185
|
}
|
|
178
186
|
/**
|
|
179
|
-
* Build a store from its
|
|
187
|
+
* Build a store from its registration, whichever form it was declared in.
|
|
180
188
|
*
|
|
181
|
-
*
|
|
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(
|
|
185
|
-
|
|
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
|
-
|
|
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 {
|
|
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?:
|
|
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.
|
|
3
|
+
"version": "0.8.16",
|
|
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.16"
|
|
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.
|
|
71
|
+
"@stone-js/config": "0.8.16"
|
|
71
72
|
},
|
|
72
73
|
"peerDependencies": {
|
|
73
|
-
"@stone-js/core": "0.8.
|
|
74
|
+
"@stone-js/core": "0.8.16"
|
|
74
75
|
},
|
|
75
76
|
"scripts": {
|
|
76
77
|
"lint": "ts-standard src",
|