Skip to content

Commit ec1a41b

Browse files
committed
feat(plugin): category
1 parent 7ce07ae commit ec1a41b

File tree

11 files changed

+473
-34
lines changed

11 files changed

+473
-34
lines changed

docs/guide/plugin/setup.md

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,38 @@ export default definePlugin({
2020

2121
The `setup` function is called when the plugin is registered. It receives a `pluginApi` object that contains useful methods to customize the store.
2222

23+
## Category
24+
25+
Plugins can be categorized to define their role in the data flow. The available categories are:
26+
27+
- `virtual`: Plugins that provide virtual/in-memory collections that do not have any persistent storage.
28+
- `local`: Plugins that handle local data sources in the current device, such as saving it to a client-side database or storage such as IndexedDB or LocalStorage.
29+
- `remote`: Plugins that handle remote data sources, such as REST APIs or GraphQL APIs.
30+
- `processing`: Plugins that process data, such as transforming or validating it.
31+
32+
By default plugins will be sorted based on their category in the following order:
33+
34+
1. `virtual`
35+
2. `local`
36+
3. `remote`
37+
4. `processing`
38+
39+
You can customize the sorting using the `before` and `after` options (see [Sorting plugins](#sorting-plugins)).
40+
41+
```ts{5-7}
42+
import { definePlugin } from '@rstore/vue'
43+
44+
export default definePlugin({
45+
name: 'my-plugin',
46+
// Will be after 'virtual' and 'local' plugins
47+
// and before 'processing' plugins
48+
category: 'remote',
49+
setup(pluginApi) {
50+
// Plugin code goes here
51+
},
52+
})
53+
```
54+
2355
## Hooks
2456

2557
Hooks are the primary way to extend the functionality of the store. They allow you to run custom code at different points in the lifecycle of the store. The hooks are called in the order they are defined.
@@ -117,14 +149,32 @@ export default definePlugin({
117149

118150
## Sorting plugins
119151

120-
Plugins are sorted based on their dependencies. You can specify that a plugin should be loaded before or after another plugin using the `before` and `after` options:
152+
Plugins are sorted based on their dependencies and category. You can specify that a plugin should be loaded before or after another plugin or category using the `before` and `after` options:
121153

122154
```ts
123155
import { definePlugin } from '@rstore/vue'
124156

125157
export default definePlugin({
126158
name: 'my-plugin',
127-
before: ['another-plugin'],
128-
after: ['yet-another-plugin'],
159+
before: {
160+
plugins: ['another-plugin'],
161+
categories: ['remote'],
162+
},
163+
after: {
164+
plugins: ['yet-another-plugin'],
165+
categories: ['virtual'],
166+
},
129167
})
130168
```
169+
170+
Each property of `before` and `after` is optional, you can either specify `plugins`, `categories`, or both.
171+
172+
::: warning
173+
Be mindful of circular dependencies when using `before` and `after`. For example, if Plugin A is set to load after Plugin B, and Plugin B is set to load after Plugin A, this will create a circular dependency that cannot be resolved. The system will detect such circular dependencies and handle them gracefully by skipping the remaining sorting rules (with a warning printed to the console).
174+
175+
Prioritization is done in the following order:
176+
177+
- `before.plugins` and `after.plugins` have the highest priority.
178+
- `before.categories` and `after.categories` have the next priority.
179+
- Default category order is applied last.
180+
:::

packages/core/src/plugin.ts

Lines changed: 107 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { CollectionDefaults, HookPayload, Plugin, RegisteredPlugin, StoreCore, StoreSchema } from '@rstore/shared'
1+
import type { CollectionDefaults, HookPayload, Plugin, PluginCategory, RegisteredPlugin, StoreCore, StoreSchema } from '@rstore/shared'
22

33
const mergedCollectionDefaultsFields = [
44
'computed',
@@ -63,18 +63,31 @@ export function definePlugin(plugin: Plugin): Plugin {
6363
return plugin
6464
}
6565

66+
const pluginCategories: PluginCategory[] = [
67+
'virtual',
68+
'local',
69+
'remote',
70+
'processing',
71+
]
72+
6673
export function sortPlugins(plugins: RegisteredPlugin[]): RegisteredPlugin[] {
6774
const pluginByName = new Map<string, RegisteredPlugin>()
6875
const beforeRelations = new Map<string, Set<string>>()
6976
const afterRelations = new Map<string, Set<string>>()
7077

78+
// Cache plugins by category for better performance
79+
const pluginsByCategory = new Map<PluginCategory, RegisteredPlugin[]>()
80+
for (const category of pluginCategories) {
81+
pluginsByCategory.set(category, plugins.filter(p => p.category === category))
82+
}
83+
7184
// Process before/after relationships
7285
for (const plugin of plugins) {
7386
pluginByName.set(plugin.name, plugin)
7487

75-
// Process 'before' relationships
76-
if (plugin.before) {
77-
for (const beforeName of plugin.before) {
88+
// Process 'before' relationships (highest priority)
89+
if (plugin.before?.plugins) {
90+
for (const beforeName of plugin.before.plugins) {
7891
if (!beforeRelations.has(plugin.name)) {
7992
beforeRelations.set(plugin.name, new Set())
8093
}
@@ -87,9 +100,9 @@ export function sortPlugins(plugins: RegisteredPlugin[]): RegisteredPlugin[] {
87100
}
88101
}
89102

90-
// Process 'after' relationships
91-
if (plugin.after) {
92-
for (const afterName of plugin.after) {
103+
// Process 'after' relationships (highest priority)
104+
if (plugin.after?.plugins) {
105+
for (const afterName of plugin.after.plugins) {
93106
if (!afterRelations.has(plugin.name)) {
94107
afterRelations.set(plugin.name, new Set())
95108
}
@@ -101,28 +114,102 @@ export function sortPlugins(plugins: RegisteredPlugin[]): RegisteredPlugin[] {
101114
beforeRelations.get(afterName)!.add(plugin.name)
102115
}
103116
}
117+
118+
// Process 'before' category relationships (medium priority)
119+
if (plugin.before?.categories) {
120+
for (const beforeCategory of plugin.before.categories) {
121+
const categoryPlugins = pluginsByCategory.get(beforeCategory as PluginCategory) || []
122+
for (const p of categoryPlugins) {
123+
if (p.name !== plugin.name) {
124+
if (!beforeRelations.has(plugin.name)) {
125+
beforeRelations.set(plugin.name, new Set())
126+
}
127+
beforeRelations.get(plugin.name)!.add(p.name)
128+
129+
if (!afterRelations.has(p.name)) {
130+
afterRelations.set(p.name, new Set())
131+
}
132+
afterRelations.get(p.name)!.add(plugin.name)
133+
}
134+
}
135+
}
136+
}
137+
138+
// Process 'after' category relationships (medium priority)
139+
if (plugin.after?.categories) {
140+
for (const afterCategory of plugin.after.categories) {
141+
const categoryPlugins = pluginsByCategory.get(afterCategory as PluginCategory) || []
142+
for (const p of categoryPlugins) {
143+
if (p.name !== plugin.name) {
144+
if (!afterRelations.has(plugin.name)) {
145+
afterRelations.set(plugin.name, new Set())
146+
}
147+
afterRelations.get(plugin.name)!.add(p.name)
148+
149+
if (!beforeRelations.has(p.name)) {
150+
beforeRelations.set(p.name, new Set())
151+
}
152+
beforeRelations.get(p.name)!.add(plugin.name)
153+
}
154+
}
155+
}
156+
}
157+
}
158+
159+
// Add default category ordering based on pluginCategories array (lowest priority)
160+
for (let i = 0; i < pluginCategories.length - 1; i++) {
161+
const currentCategory = pluginCategories[i]!
162+
const nextCategory = pluginCategories[i + 1]!
163+
164+
const currentPlugins = pluginsByCategory.get(currentCategory) || []
165+
const nextPlugins = pluginsByCategory.get(nextCategory) || []
166+
167+
for (const current of currentPlugins) {
168+
for (const next of nextPlugins) {
169+
if (!beforeRelations.has(current.name)) {
170+
beforeRelations.set(current.name, new Set())
171+
}
172+
beforeRelations.get(current.name)!.add(next.name)
173+
174+
if (!afterRelations.has(next.name)) {
175+
afterRelations.set(next.name, new Set())
176+
}
177+
afterRelations.get(next.name)!.add(current.name)
178+
}
179+
}
104180
}
105181

106182
// Topological sort using depth-first search
107183
const sorted: RegisteredPlugin[] = []
108184
const visited = new Set<string>()
109185
const visiting = new Set<string>()
186+
const circularDependencies = new Set<string>()
110187

111-
function visit(name: string) {
188+
function visit(name: string, path: string[] = []) {
112189
if (visited.has(name))
113-
return
190+
return true
191+
if (circularDependencies.has(name))
192+
return false
114193
if (visiting.has(name)) {
115-
throw new Error(`Circular dependency detected for plugin ${name}`)
194+
const cycle = [...path, name].join(' -> ')
195+
console.warn(`[rstore] Circular dependency detected: ${cycle}`)
196+
circularDependencies.add(name)
197+
return false
116198
}
117199

118200
visiting.add(name)
201+
path.push(name)
119202

120203
// Process 'after' dependencies first (they should come before this plugin)
121204
const afterDeps = afterRelations.get(name)
122205
if (afterDeps) {
123206
for (const afterName of afterDeps) {
124207
if (pluginByName.has(afterName)) {
125-
visit(afterName)
208+
if (!visit(afterName, [...path])) {
209+
// If we encounter a circular dependency, we'll still continue with other plugins
210+
// but skip this dependency
211+
console.warn(`[rstore] Skipping circular dependency from ${name} to ${afterName}`)
212+
}
126213
}
127214
}
128215
}
@@ -134,6 +221,8 @@ export function sortPlugins(plugins: RegisteredPlugin[]): RegisteredPlugin[] {
134221
if (plugin) {
135222
sorted.push(plugin)
136223
}
224+
225+
return true
137226
}
138227

139228
// Process all plugins
@@ -143,5 +232,12 @@ export function sortPlugins(plugins: RegisteredPlugin[]): RegisteredPlugin[] {
143232
}
144233
}
145234

235+
// Handle any remaining plugins that weren't visited due to circular dependencies
236+
for (const plugin of plugins) {
237+
if (!sorted.includes(plugin)) {
238+
sorted.push(plugin)
239+
}
240+
}
241+
146242
return sorted
147243
}

0 commit comments

Comments
 (0)