pinia-plugin-subscription 0.2.0-beta.0 → 0.2.1
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/CHANGELOG.md +28 -1
- package/README.md +49 -341
- package/dist/chunks/{index-D-rnwSJk.js → index-DKdl-fiq.js} +52 -58
- package/dist/chunks/index-DKdl-fiq.js.map +1 -0
- package/dist/core/PluginSubscription.d.ts +2 -0
- package/dist/core/RollbackStateSnapshotsHandler.d.ts +1 -0
- package/dist/helpers.js +2 -1
- package/dist/helpers.js.map +1 -0
- package/dist/lib/main.d.ts +1 -1
- package/dist/pinia-plugin-subscription.js +103 -161
- package/dist/pinia-plugin-subscription.js.map +1 -0
- package/dist/plugins/createPlugin.d.ts +4 -1
- package/dist/tests/defineAStore.test-d.d.ts +1 -0
- package/dist/tests/storeOptionsExtensions.test-d.d.ts +12 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/store.d.ts +39 -2
- package/dist/utils/deep-clone.d.ts +6 -0
- package/dist/utils/store.d.ts +77 -8
- package/docs/authoring-plugins.md +408 -0
- package/docs/using-plugins.md +375 -0
- package/package.json +4 -1
package/dist/types/store.d.ts
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
1
|
import type { AnyObject } from ".";
|
|
2
2
|
import type { CreateStateSnapshotKeys } from "./plugin";
|
|
3
|
-
import type { PiniaCustomProperties, Store, StoreDefinition } from "pinia";
|
|
3
|
+
import type { Pinia, PiniaCustomProperties, StateTree, Store, StoreDefinition, StoreGeneric } from "pinia";
|
|
4
4
|
import type { Ref } from "vue";
|
|
5
5
|
export type CustomStore<TStore, TState> = Store & TStore & TState & PiniaCustomProperties & StoreDefinition;
|
|
6
|
+
/**
|
|
7
|
+
* Store definition returned by `defineAStore`/`defineAStoreCtx` when explicit
|
|
8
|
+
* store/state types are provided, e.g. for stores augmented by plugins with
|
|
9
|
+
* members not returned by the setup function.
|
|
10
|
+
*
|
|
11
|
+
* The call signature exposes `TStore` (actions/getters) and `TState` (state)
|
|
12
|
+
* with precise typings so IDEs list store members and display action
|
|
13
|
+
* prototypes. It extends pinia's `StoreDefinition` to stay assignable
|
|
14
|
+
* wherever a standard store definition is expected.
|
|
15
|
+
*/
|
|
16
|
+
export interface DefineAStoreDefinition<TStore, TState, Id extends string = string> extends StoreDefinition<Id, StrictStateTree<TState>> {
|
|
17
|
+
/**
|
|
18
|
+
* Returns a store, creates it if necessary.
|
|
19
|
+
*
|
|
20
|
+
* @param pinia - Pinia instance to retrieve the store
|
|
21
|
+
* @param hot - dev only hot module replacement
|
|
22
|
+
*/
|
|
23
|
+
(pinia?: Pinia | null, hot?: StoreGeneric): Store<Id, StrictStateTree<TState>> & TStore;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Maps a state interface to a pinia `StateTree` without intersecting it with
|
|
27
|
+
* `StateTree` itself, whose `any` index signature would widen every store member to `any`.
|
|
28
|
+
*/
|
|
29
|
+
export type StrictStateTree<TState> = {
|
|
30
|
+
[K in keyof TState]: TState[K];
|
|
31
|
+
} extends infer S extends StateTree ? S : never;
|
|
6
32
|
export type EmptyExtensions = Record<never, never>;
|
|
7
33
|
export interface DefineAStoreSetupExtensions<TEnhancedStore> {
|
|
8
34
|
/**
|
|
@@ -30,7 +56,18 @@ export type RollbackAfterFailureParams = {
|
|
|
30
56
|
};
|
|
31
57
|
export type StatePropertyValue = StdStatePropertyValue | Ref<StdStatePropertyValue> | StdStatePropertyValue[] | Ref<StdStatePropertyValue[]> | Ref<StdStatePropertyValue>[];
|
|
32
58
|
type StdStatePropertyValue = AnyObject | boolean | null | number | string | undefined;
|
|
33
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Centralized extension point for per-store plugin options.
|
|
61
|
+
*
|
|
62
|
+
* Subscriber plugins MUST NOT redeclare pinia's `DefineStoreOptionsBase`:
|
|
63
|
+
* merging a `storeOptions` property with a different (even compatible) type
|
|
64
|
+
* raises TS2717 in consumer code. Instead, they augment this interface via
|
|
65
|
+
* `declare module 'pinia-plugin-subscription/types'`, which merges safely as
|
|
66
|
+
* long as each plugin declares its own optional keys.
|
|
67
|
+
*/
|
|
68
|
+
export interface StoreOptionsExtensions {
|
|
69
|
+
}
|
|
70
|
+
export interface StoreOptions extends StoreOptionsExtensions {
|
|
34
71
|
[key: string]: StoreOptionsPropertyValue;
|
|
35
72
|
rollbackAfterFailure?: RollbackAfterFailureParams;
|
|
36
73
|
}
|
|
@@ -1 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deeply clones the provided value, handling objects, arrays, maps, and sets. Uses `structuredClone` if available, otherwise falls back to a manual deep clone.
|
|
3
|
+
* @template T The type of the value to deep clone.
|
|
4
|
+
* @param value The value to deep clone.
|
|
5
|
+
* @returns A deep clone of the provided value.
|
|
6
|
+
*/
|
|
1
7
|
export declare function deepClone<T>(value: T): T;
|
package/dist/utils/store.d.ts
CHANGED
|
@@ -1,25 +1,94 @@
|
|
|
1
|
-
import type { DefineStoreOptions, StateTree, Store, StoreDefinition } from "pinia";
|
|
1
|
+
import type { DefineStoreOptions, SetupStoreDefinition, StateTree, Store, StoreDefinition, _GettersTree } from "pinia";
|
|
2
2
|
import type { AnyObject } from "../types";
|
|
3
|
-
import type { DefineAStoreSetupContext, EmptyExtensions, PluginStoreOptions, StoreOptions } from "../types/store";
|
|
3
|
+
import type { DefineAStoreDefinition, DefineAStoreSetupContext, EmptyExtensions, PluginStoreOptions, StoreOptions } from "../types/store";
|
|
4
4
|
export declare const itemState: {
|
|
5
5
|
'@id': undefined;
|
|
6
6
|
id: undefined;
|
|
7
7
|
};
|
|
8
|
-
type DefineAStoreSetup<
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
type DefineAStoreSetup<SS, TExtraExtensions extends Record<string, unknown> = EmptyExtensions> = (ctx: DefineAStoreSetupContext<SS, TExtraExtensions>) => SS;
|
|
9
|
+
/**
|
|
10
|
+
* Define a store with the setup API. State, getters and actions are inferred
|
|
11
|
+
* from the setup function return, exactly like pinia's `defineStore`, so
|
|
12
|
+
* store members are listed by IDEs and action prototypes are preserved.
|
|
13
|
+
* @template Id The type of the store identifier.
|
|
14
|
+
* @template SS The type of the setup function return.
|
|
15
|
+
* @param id The unique identifier for the store.
|
|
16
|
+
* @param storeDefinition The setup function defining the store.
|
|
17
|
+
* @param options Optional store options.
|
|
18
|
+
* @returns The defined store.
|
|
19
|
+
*/
|
|
20
|
+
export declare function defineAStore<Id extends string, SS>(id: Id, storeDefinition: (ctx?: DefineAStoreSetupContext<AnyObject>) => SS, options?: StoreOptions): SetupStoreDefinition<Id, SS>;
|
|
21
|
+
/**
|
|
22
|
+
* Define a store with the options API, mirroring pinia's `defineStore` inference.
|
|
23
|
+
* @template Id The type of the store identifier.
|
|
24
|
+
* @template S The type of the store's state.
|
|
25
|
+
* @template G The type of the store's getters.
|
|
26
|
+
* @template A The type of the store's actions.
|
|
27
|
+
* @param id The unique identifier for the store.
|
|
28
|
+
* @param storeDefinition The store options definition.
|
|
29
|
+
* @param options Optional store options.
|
|
30
|
+
* @returns The defined store.
|
|
31
|
+
*/
|
|
32
|
+
export declare function defineAStore<Id extends string, S extends StateTree = {}, G extends _GettersTree<S> = {}, A = {}>(id: Id, storeDefinition: Omit<DefineStoreOptions<Id, S, G, A>, 'id'>, options?: StoreOptions): StoreDefinition<Id, S, G, A>;
|
|
33
|
+
/**
|
|
34
|
+
* Define a store with explicit store/state types, e.g. when plugins augment
|
|
35
|
+
* the store with members not returned by the setup function.
|
|
36
|
+
* @template Sto The type of the store.
|
|
37
|
+
* @template Sta The type of the store's state.
|
|
38
|
+
* @param id The unique identifier for the store.
|
|
39
|
+
* @param storeDefinition The store definition, either as an options object or a setup function.
|
|
40
|
+
* @param options Optional store options.
|
|
41
|
+
* @returns The defined store.
|
|
42
|
+
*/
|
|
43
|
+
export declare function defineAStore<Sto, Sta>(id: string, storeDefinition: Omit<DefineStoreOptions<string, StateTree & Sta, AnyObject, Partial<Sto>>, 'id'> | ((ctx?: DefineAStoreSetupContext<Sto & Sta>) => AnyObject), options?: StoreOptions): DefineAStoreDefinition<Sto, Sta>;
|
|
44
|
+
/**
|
|
45
|
+
* Define a store with a required, fully typed setup context. State, getters
|
|
46
|
+
* and actions are inferred from the setup function return.
|
|
47
|
+
* @template Id The type of the store identifier.
|
|
48
|
+
* @template SS The type of the setup function return.
|
|
49
|
+
* @template TExtraExtensions The type of the store's extra extensions.
|
|
50
|
+
* @param id The unique identifier for the store.
|
|
51
|
+
* @param storeDefinition The setup function defining the store.
|
|
52
|
+
* @param options Optional store options.
|
|
53
|
+
* @returns The defined store with setup context.
|
|
54
|
+
*/
|
|
55
|
+
export declare function defineAStoreCtx<Id extends string, SS, TExtraExtensions extends Record<string, unknown> = EmptyExtensions>(id: Id, storeDefinition: (ctx: DefineAStoreSetupContext<AnyObject, TExtraExtensions>) => SS, options?: StoreOptions): SetupStoreDefinition<Id, SS>;
|
|
56
|
+
/**
|
|
57
|
+
* Define a store with a setup context and explicit store/state types, e.g.
|
|
58
|
+
* when plugins augment the store with members not returned by the setup function.
|
|
59
|
+
* @template Sto The type of the store.
|
|
60
|
+
* @template Sta The type of the store's state.
|
|
61
|
+
* @template TExtraExtensions The type of the store's extra extensions.
|
|
62
|
+
* @param id The unique identifier for the store.
|
|
63
|
+
* @param storeDefinition The setup function defining the store.
|
|
64
|
+
* @param options Optional store options.
|
|
65
|
+
* @returns The defined store with setup context.
|
|
66
|
+
*/
|
|
67
|
+
export declare function defineAStoreCtx<Sto, Sta, TExtraExtensions extends Record<string, unknown> = EmptyExtensions>(id: string, storeDefinition: (ctx: DefineAStoreSetupContext<Sto & Sta, TExtraExtensions>) => AnyObject, options?: StoreOptions): DefineAStoreDefinition<Sto, Sta>;
|
|
68
|
+
export declare function defineAStoreSetup<Id extends string, SS>(id: Id, storeDefinition: DefineAStoreSetup<SS>, options: PluginStoreOptions): SetupStoreDefinition<Id, SS>;
|
|
69
|
+
export declare function defineAStoreOptionApi<Id extends string, S extends StateTree = {}, G extends _GettersTree<S> = {}, A = {}>(id: Id, storeDefinition: Omit<DefineStoreOptions<Id, S, G, A>, 'id'>, options?: PluginStoreOptions): StoreDefinition<Id, S, G, A>;
|
|
70
|
+
/**
|
|
71
|
+
* Check if a property has a denied first character : ['_', '$'].
|
|
72
|
+
* @param property The property name to check.
|
|
73
|
+
* @returns True if the property has a denied first character, false otherwise.
|
|
74
|
+
*/
|
|
13
75
|
export declare function hasDeniedFirstChar(property: string): boolean;
|
|
14
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Get the setup context associated with a store instance (or pending store id).
|
|
78
|
+
* @param store The store instance, or an object exposing the store `$id`.
|
|
79
|
+
* @returns The setup context when the store was defined with `enhancedStore` enabled, undefined otherwise.
|
|
80
|
+
*/
|
|
81
|
+
export declare function getDefineAStoreSetupContext(store: object): DefineAStoreSetupContext<AnyObject> | undefined;
|
|
15
82
|
/**
|
|
16
83
|
* Get the enhanced store from the context.
|
|
84
|
+
* @template TEnhancedStore The type of the enhanced store.
|
|
17
85
|
* @param ctx The context containing the extensions.
|
|
18
86
|
* @returns The enhanced store casted to the specified types.
|
|
19
87
|
*/
|
|
20
88
|
export declare function getEnhancedStore<TEnhancedStore>(ctx: DefineAStoreSetupContext<TEnhancedStore>): TEnhancedStore;
|
|
21
89
|
/**
|
|
22
90
|
* Set the enhanced store in context with deprecated alias support.
|
|
91
|
+
* @template TEnhancedStore The type of the enhanced store.
|
|
23
92
|
* @param ctx The context containing the extensions.
|
|
24
93
|
* @param store The store to expose as enhancement.
|
|
25
94
|
*/
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
# Plugin authoring guide
|
|
2
|
+
|
|
3
|
+
Guide for **plugin authors** building their own pinia plugin on top of
|
|
4
|
+
`pinia-plugin-subscription`.
|
|
5
|
+
|
|
6
|
+
If you only want to **use** compatible plugins in an application, read
|
|
7
|
+
[Using compatible plugins](using-plugins.md) instead.
|
|
8
|
+
|
|
9
|
+
> **Documentation convention:** JavaScript examples come first and are
|
|
10
|
+
> complete and runnable as-is. TypeScript versions follow, adding type
|
|
11
|
+
> annotations and generics. Types are always optional at runtime — they only
|
|
12
|
+
> add autocompletion and compile-time checks.
|
|
13
|
+
|
|
14
|
+
## Mental model
|
|
15
|
+
|
|
16
|
+
- A **subscriber** is invoked by the central `PluginSubscription` engine
|
|
17
|
+
each time pinia registers a store.
|
|
18
|
+
- A subscriber can extend the store, register callbacks (mutation
|
|
19
|
+
subscriptions, action subscriptions, reset callbacks), and declare an
|
|
20
|
+
execution policy for SSR.
|
|
21
|
+
- `createPlugin([...subscribers])` aggregates subscribers into a single
|
|
22
|
+
pinia plugin.
|
|
23
|
+
|
|
24
|
+
Two authoring styles are available:
|
|
25
|
+
|
|
26
|
+
1. **Plain object** implementing `PluginSubscriberInterface` — lightweight,
|
|
27
|
+
no classes.
|
|
28
|
+
2. **`PluginSubscriber` abstract class + `Store` subclass** — recommended
|
|
29
|
+
for richer plugins: the `Store` subclass wraps the pinia store with
|
|
30
|
+
helpers (state extension, subscriptions, debug logging).
|
|
31
|
+
|
|
32
|
+
## Your first subscriber: a plain object
|
|
33
|
+
|
|
34
|
+
### JavaScript
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
export const myStoreSubscriber = {
|
|
38
|
+
name: 'my-plugin',
|
|
39
|
+
|
|
40
|
+
invoke: (context, debug) => {
|
|
41
|
+
// context contains `store`, `options`, `pinia`
|
|
42
|
+
console.log('store registered', context.store.$id)
|
|
43
|
+
|
|
44
|
+
return true
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
resetStoreCallback: (store) => {
|
|
48
|
+
console.log('store reset:', store.$id)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### TypeScript
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import type { PluginSubscriberInterface } from 'pinia-plugin-subscription'
|
|
57
|
+
|
|
58
|
+
export const myStoreSubscriber: PluginSubscriberInterface = {
|
|
59
|
+
name: 'my-plugin',
|
|
60
|
+
|
|
61
|
+
invoke: (context, debug) => {
|
|
62
|
+
console.log('store registered', context.store.$id)
|
|
63
|
+
|
|
64
|
+
return true
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
resetStoreCallback: (store) => {
|
|
68
|
+
console.log('store reset:', store.$id)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The host app registers subscribers through `createPlugin()`:
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
import { createPlugin } from 'pinia-plugin-subscription'
|
|
77
|
+
|
|
78
|
+
pinia.use(createPlugin([myStoreSubscriber]))
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## `PluginSubscriberInterface` reference
|
|
82
|
+
|
|
83
|
+
| Member | Type | Description |
|
|
84
|
+
| --- | --- | --- |
|
|
85
|
+
| `name` (required) | `string` | Unique plugin name; also used by the debug filter. |
|
|
86
|
+
| `invoke` (required) | `(context: PiniaPluginContext, debug: boolean) => boolean` | Called when a store is registered. Return `false` to skip the store. |
|
|
87
|
+
| `execution` | `PluginExecutionOptions` | Where/when the subscriber runs — see [Execution policy & SSR](#execution-policy--ssr). |
|
|
88
|
+
| `hydrationScheduler` | `(run: () => void) => void` | Custom scheduler used when hydration is deferred. |
|
|
89
|
+
| `hydrate` | `(context, debug) => void \| Promise<void>` | SSR-safe initialization hook; guaranteed to run before `afterHydration`. |
|
|
90
|
+
| `afterHydration` | `(context, debug) => void \| Promise<void>` | Post-hydration lifecycle hook. |
|
|
91
|
+
| `resetStoreCallback` | `(store?: Store) => void` | Custom logic executed when a registered store is reset. |
|
|
92
|
+
| `storeOnActionSubscription` | `{ store, callback }` (getter) | Action subscription forwarded to the returned store's `$onAction`, including after deferred hydration; removed on store disposal. |
|
|
93
|
+
| `storeMutationSubscription` | `{ store, callback }` (getter) | Mutation subscription forwarded to pinia's `$subscribe`. |
|
|
94
|
+
| `subscriptions` | `Record<string, PluginSubscription>` | Plugin-specific subscription functions exposed to other plugins. |
|
|
95
|
+
| `console` | `Console` | Custom console used for debug logging. |
|
|
96
|
+
|
|
97
|
+
All members except `name` and `invoke` are optional.
|
|
98
|
+
|
|
99
|
+
## The `PluginSubscriber` abstract class
|
|
100
|
+
|
|
101
|
+
For class-based plugins, extend `PluginSubscriber<Instance>`. The subscriber
|
|
102
|
+
creates the instance through a factory — conventionally
|
|
103
|
+
`MyPlugin.customizeStore.bind(MyPlugin)`.
|
|
104
|
+
|
|
105
|
+
### Type parameter (TypeScript only)
|
|
106
|
+
|
|
107
|
+
| Generic | Constraint | Role |
|
|
108
|
+
| --- | --- | --- |
|
|
109
|
+
| `Instance` | `extends Store` | The `Store` subclass your plugin creates for each registered store. |
|
|
110
|
+
|
|
111
|
+
The generic parameter is optional — JavaScript code and untyped TypeScript
|
|
112
|
+
plugins can extend the class without it. Providing it types the
|
|
113
|
+
`storeInstance` getter, so overridden hooks (`hydrate()`,
|
|
114
|
+
`afterHydration()`, `pluginCreated`) get full autocompletion and compile-time
|
|
115
|
+
checks when they call your plugin's custom methods.
|
|
116
|
+
|
|
117
|
+
Constructor:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
constructor(pluginName: string, createInstance: CreateInstance, pluginConsole?: Console)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
| Member | Kind | Description |
|
|
124
|
+
| --- | --- | --- |
|
|
125
|
+
| `execution` | public field | Execution policy (see below). |
|
|
126
|
+
| `hydrationScheduler` | public field | Custom scheduler used when hydration is deferred. |
|
|
127
|
+
| `pluginCreated` | protected field | Optional hook called with the pinia store after a successful `invoke`. |
|
|
128
|
+
| `pluginOptions` | getter/setter | Extra options merged into the options passed to your `Store` factory. |
|
|
129
|
+
| `storeInstance` | getter | The `Store` subclass instance created for the current store. |
|
|
130
|
+
| `hydrate()` / `afterHydration()` | methods | Lifecycle stubs to override for SSR-safe initialization. |
|
|
131
|
+
| `invoke()` | method | Implemented for you: creates the instance, wires `subscriptions`, `storeSubscribe` and `onAction`, then calls `pluginCreated`. Returns `false` when the factory creates no instance. |
|
|
132
|
+
|
|
133
|
+
### JavaScript
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
import { PluginSubscriber, Store } from 'pinia-plugin-subscription'
|
|
137
|
+
|
|
138
|
+
class MyPlugin extends Store {
|
|
139
|
+
constructor(store, options, debug = false) {
|
|
140
|
+
super(store, options, debug)
|
|
141
|
+
this.doSomething()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
doSomething() {
|
|
145
|
+
this.debugLog('doSomething', this.store.$id)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
class MyPluginSubscriber extends PluginSubscriber {
|
|
150
|
+
constructor() {
|
|
151
|
+
super('my-plugin', (store, options, debug, customConsole) =>
|
|
152
|
+
new MyPlugin(store, options, debug, customConsole))
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export const myStoreSubscriber = new MyPluginSubscriber()
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### TypeScript
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
import { PluginSubscriber, Store } from 'pinia-plugin-subscription'
|
|
163
|
+
|
|
164
|
+
class MyPlugin extends Store {
|
|
165
|
+
protected override _className: string = 'MyPlugin'
|
|
166
|
+
protected static override _requiredKeys?: string[] = ['my-plugin-option']
|
|
167
|
+
|
|
168
|
+
constructor(store, options, debug = false) {
|
|
169
|
+
super(store, options, debug)
|
|
170
|
+
this.doSomething()
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
doSomething(): void {
|
|
174
|
+
this.debugLog('doSomething', this.store.$id)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
class MyPluginSubscriber extends PluginSubscriber<MyPlugin> {
|
|
179
|
+
constructor() {
|
|
180
|
+
super('my-plugin', MyPlugin.customizeStore.bind(MyPlugin))
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export const myStoreSubscriber = new MyPluginSubscriber()
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
> **Important contract:** `Store.customizeStore()` only creates an instance
|
|
188
|
+
> when the store defines `storeOptions` **and** every key listed in the
|
|
189
|
+
> static `_requiredKeys` is present in those options. When no instance is
|
|
190
|
+
> created, `invoke()` returns `false` and the store is skipped by your
|
|
191
|
+
> plugin. Declare `_requiredKeys = []` to accept every store that defines
|
|
192
|
+
> `storeOptions`.
|
|
193
|
+
|
|
194
|
+
## The `Store` base class
|
|
195
|
+
|
|
196
|
+
`Store` wraps a pinia store (`PiniaStore`) and exposes:
|
|
197
|
+
|
|
198
|
+
**Properties:** `debug`, `options`, `state`, `store`.
|
|
199
|
+
|
|
200
|
+
**Methods:**
|
|
201
|
+
|
|
202
|
+
| Method | Description |
|
|
203
|
+
| --- | --- |
|
|
204
|
+
| `addToState(name, value?)` | Adds a property to the store state; exposed as a `Ref` for setup-style stores. |
|
|
205
|
+
| `addSubscription(pluginName, subscription, options?)` | Registers a plugin-specific subscription function. |
|
|
206
|
+
| `getSubscriptions()` | Returns registered subscriptions (`undefined` when empty). |
|
|
207
|
+
| `getOption(optionName)` | Reads one key from the store's `StoreOptions`. |
|
|
208
|
+
| `getStatePropertyValue(propertyName)` | Reads a state property, unwrapping refs. |
|
|
209
|
+
| `storeSubscribe` (getter/setter) | Factory for pinia's `$subscribe` (`{ store, callback }`). |
|
|
210
|
+
| `onAction` (getter/setter) | Factory for pinia's `$onAction` (`{ store, callback }`). |
|
|
211
|
+
| `static customizeStore(store, options, debug?)` | Recommended factory; guarded by `_requiredKeys` (see above). |
|
|
212
|
+
| `debugLog(message, args)` | Conditional logging, active in debug mode. |
|
|
213
|
+
| `stateHas(property)` / `storeHas(property)` | Safe `hasOwnProperty` checks on state / store. |
|
|
214
|
+
| `getValue(value)` | Unwraps a ref or returns the value as-is. |
|
|
215
|
+
| `isOptionApi()` | Whether the store uses the options API. |
|
|
216
|
+
| `hydrate()` / `afterHydration()` | Lifecycle stubs for SSR-safe initialization. |
|
|
217
|
+
|
|
218
|
+
## Execution policy & SSR
|
|
219
|
+
|
|
220
|
+
The source of truth for hydration policy stays on the subscriber itself.
|
|
221
|
+
For example, a client-only plugin declares its execution directly:
|
|
222
|
+
|
|
223
|
+
### JavaScript
|
|
224
|
+
|
|
225
|
+
```js
|
|
226
|
+
import { nextTick } from 'vue'
|
|
227
|
+
|
|
228
|
+
export const persistedStateSubscriber = {
|
|
229
|
+
name: 'persisted-state',
|
|
230
|
+
execution: {
|
|
231
|
+
environment: 'client',
|
|
232
|
+
hydration: 'defer',
|
|
233
|
+
},
|
|
234
|
+
hydrationScheduler: (run) => {
|
|
235
|
+
nextTick(() => run())
|
|
236
|
+
},
|
|
237
|
+
invoke: (context, debug) => {
|
|
238
|
+
// ...
|
|
239
|
+
return true
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### TypeScript
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
import { nextTick } from 'vue'
|
|
248
|
+
import type { PluginSubscriberInterface } from 'pinia-plugin-subscription'
|
|
249
|
+
|
|
250
|
+
export const persistedStateSubscriber: PluginSubscriberInterface = {
|
|
251
|
+
name: 'persisted-state',
|
|
252
|
+
execution: {
|
|
253
|
+
environment: 'client',
|
|
254
|
+
hydration: 'defer',
|
|
255
|
+
},
|
|
256
|
+
hydrationScheduler: (run) => {
|
|
257
|
+
nextTick(() => run())
|
|
258
|
+
},
|
|
259
|
+
invoke: (context, debug) => {
|
|
260
|
+
// ...
|
|
261
|
+
return true
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Available execution options (`PluginExecutionOptions`):
|
|
267
|
+
|
|
268
|
+
- `environment: 'both' | 'client' | 'server'` — controls where the
|
|
269
|
+
subscriber is allowed to run.
|
|
270
|
+
- `hydration: 'immediate' | 'defer'` — on the client, `defer` schedules
|
|
271
|
+
execution after hydration.
|
|
272
|
+
- `hydrationScheduler` can be provided either on a subscriber or via
|
|
273
|
+
`createHydrationPlugin()` to override the execution timing when needed.
|
|
274
|
+
|
|
275
|
+
The class-based equivalent sets the same policy in the constructor and
|
|
276
|
+
overrides the lifecycle hooks:
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
import { nextTick } from 'vue'
|
|
280
|
+
import { PluginSubscriber } from 'pinia-plugin-subscription'
|
|
281
|
+
|
|
282
|
+
class ExtendingStoreSubscriber extends PluginSubscriber<StoreExtension> {
|
|
283
|
+
constructor() {
|
|
284
|
+
super('extendsPiniaStore', StoreExtension.customizeStore.bind(StoreExtension))
|
|
285
|
+
|
|
286
|
+
// Controls when this subscriber may run and whether it should wait
|
|
287
|
+
// for hydration on the client.
|
|
288
|
+
this.execution = {
|
|
289
|
+
environment: 'client',
|
|
290
|
+
hydration: 'defer',
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Optional framework-specific scheduler for deferred execution.
|
|
294
|
+
this.hydrationScheduler = (run) => {
|
|
295
|
+
nextTick(() => run())
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
this.pluginCreated = addStore
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
override hydrate() {
|
|
302
|
+
if (typeof window === 'undefined') {
|
|
303
|
+
return
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Safe browser-only initialization, only called when the policy allows it.
|
|
307
|
+
return this.storeInstance?.hydrate?.()
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### SSR / Nuxt best practices
|
|
313
|
+
|
|
314
|
+
`pinia-plugin-subscription` is SSR-safe by design, but the runtime policy
|
|
315
|
+
still needs to match the actual app lifecycle.
|
|
316
|
+
|
|
317
|
+
Use `hydration: 'defer'` when the subscriber depends on browser-only APIs,
|
|
318
|
+
client-side cookies, `localStorage`, DOM access, or a hydrated app state
|
|
319
|
+
that must exist only after the app is mounted.
|
|
320
|
+
|
|
321
|
+
Use `runtimeEnvironment: 'server'` (on `createHydrationPlugin()`) or
|
|
322
|
+
`execution.environment: 'server'` when a subscriber is only valid during
|
|
323
|
+
server-side rendering or should not touch browser-only objects:
|
|
324
|
+
|
|
325
|
+
```js
|
|
326
|
+
pinia.use(createHydrationPlugin([serverOnlySubscriber], {
|
|
327
|
+
runtimeEnvironment: 'server'
|
|
328
|
+
}))
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Avoid browser-only code in stores during build-time SSR. In practice:
|
|
332
|
+
|
|
333
|
+
- do not access `window`, `document`, `localStorage`, or `matchMedia` at
|
|
334
|
+
module scope or during store setup when the code may run on the server;
|
|
335
|
+
- keep browser-specific bootstrap in `hydrate()` or `afterHydration()`;
|
|
336
|
+
- prefer environment-aware guards such as `typeof window !== 'undefined'`;
|
|
337
|
+
- avoid network access or hydration reads before the client is ready,
|
|
338
|
+
unless the code is explicitly intended to run on both environments.
|
|
339
|
+
|
|
340
|
+
A safe pattern:
|
|
341
|
+
|
|
342
|
+
```js
|
|
343
|
+
const mySubscriber = {
|
|
344
|
+
name: 'safe-browser-access',
|
|
345
|
+
execution: {
|
|
346
|
+
environment: 'client',
|
|
347
|
+
hydration: 'defer',
|
|
348
|
+
},
|
|
349
|
+
hydrate: (context) => {
|
|
350
|
+
// safe browser access here, after hydration is allowed
|
|
351
|
+
const saved = localStorage.getItem('demo')
|
|
352
|
+
if (saved) {
|
|
353
|
+
context.store.$patch({ value: JSON.parse(saved) })
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
invoke: () => true,
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
This keeps the server render deterministic and prevents browser-only
|
|
361
|
+
initialization from crashing Nuxt or SSR builds.
|
|
362
|
+
|
|
363
|
+
## Typing per-store options: `StoreOptionsExtensions` (TypeScript)
|
|
364
|
+
|
|
365
|
+
This package is the only one allowed to augment pinia's
|
|
366
|
+
`DefineStoreOptionsBase`. A subscriber plugin must **not** redeclare it:
|
|
367
|
+
merging `storeOptions` with a different (even compatible) type raises
|
|
368
|
+
`TS2717` in consumer code.
|
|
369
|
+
|
|
370
|
+
Instead, per-store plugin options are centralized behind the
|
|
371
|
+
`StoreOptionsExtensions` interface. Augment it through interface merging to
|
|
372
|
+
add your own typed keys:
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
// my-subscriber-plugin/types.d.ts (shipped with your plugin)
|
|
376
|
+
declare module 'pinia-plugin-subscription/types' {
|
|
377
|
+
interface StoreOptionsExtensions {
|
|
378
|
+
myPluginOption?: string
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
The keys merge into `StoreOptions`, so store authors get type-checking
|
|
384
|
+
directly in the store definition:
|
|
385
|
+
|
|
386
|
+
```ts
|
|
387
|
+
export const useMyStore = defineAStore('myStore', () => { /* ... */ }, {
|
|
388
|
+
rollbackAfterFailure: { save: 'all' }, // built-in option
|
|
389
|
+
myPluginOption: 'value', // typed via your augmentation
|
|
390
|
+
})
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
Guidelines:
|
|
394
|
+
|
|
395
|
+
- Always declare **optional** keys with names specific enough to avoid
|
|
396
|
+
collisions (prefer `myPluginOption` over `options`).
|
|
397
|
+
- At runtime, merged options are delivered to your subscriber's
|
|
398
|
+
`invoke(context, debug)` through `context.options.storeOptions` — no
|
|
399
|
+
extra wiring needed.
|
|
400
|
+
- Never redeclare `DefineStoreOptionsBase` or `PiniaCustomProperties`; only
|
|
401
|
+
this package owns those augmentations.
|
|
402
|
+
|
|
403
|
+
## Reference implementation
|
|
404
|
+
|
|
405
|
+
The demo app ships a complete plugin implementation in
|
|
406
|
+
[`src/demo/extending-pinia-store/`](../src/demo/extending-pinia-store/): a
|
|
407
|
+
`PluginSubscriber` subclass, a `Store` subclass, option typing and SSR
|
|
408
|
+
guards. Run `npm run dev` to explore it live.
|