@storix-js/sync 0.1.0 → 0.1.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/README.md +22 -0
- package/package.json +9 -2
- package/src/index.ts +0 -161
- package/tsconfig.build.json +0 -8
- package/tsconfig.json +0 -4
- package/vite.config.ts +0 -15
package/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# @storix-js/sync
|
|
2
|
+
|
|
3
|
+
Cross-tab synchronization plugin for Storix.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @storix-js/core @storix-js/sync
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { createStorix } from '@storix-js/core';
|
|
15
|
+
import { syncPlugin } from '@storix-js/sync';
|
|
16
|
+
|
|
17
|
+
const storix = createStorix({
|
|
18
|
+
plugins: [syncPlugin({ debounce: 16, namespaces: ['shell-app'] })]
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The plugin uses `BroadcastChannel` when available and falls back to the `storage` event.
|
package/package.json
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@storix-js/sync",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.cjs",
|
|
6
6
|
"module": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
8
15
|
"exports": {
|
|
9
16
|
".": {
|
|
10
17
|
"types": "./dist/index.d.ts",
|
|
@@ -19,6 +26,6 @@
|
|
|
19
26
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
20
27
|
},
|
|
21
28
|
"dependencies": {
|
|
22
|
-
"@storix-js/core": "
|
|
29
|
+
"@storix-js/core": "^0.1.1"
|
|
23
30
|
}
|
|
24
31
|
}
|
package/src/index.ts
DELETED
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import { definePlugin, type MutationPayload } from '@storix-js/core';
|
|
2
|
-
|
|
3
|
-
export interface SyncPluginOptions {
|
|
4
|
-
channelName?: string;
|
|
5
|
-
include?: string[];
|
|
6
|
-
exclude?: string[];
|
|
7
|
-
namespaces?: string[] | '*';
|
|
8
|
-
conflict?: 'last-write-wins';
|
|
9
|
-
debounce?: number;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
type ScopedStore = {
|
|
13
|
-
$id: string;
|
|
14
|
-
$namespace: string;
|
|
15
|
-
$scopeId: string;
|
|
16
|
-
$patch: (patch: Record<string, unknown>, origin?: 'local' | 'remote') => void;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export function syncPlugin(options: SyncPluginOptions = {}) {
|
|
20
|
-
const channelName = options.channelName ?? 'storix-sync';
|
|
21
|
-
const include = new Set(options.include ?? []);
|
|
22
|
-
const exclude = new Set(options.exclude ?? []);
|
|
23
|
-
const namespaceSelection = options.namespaces ?? [];
|
|
24
|
-
const allowedNamespaces = namespaceSelection === '*' ? null : new Set(namespaceSelection);
|
|
25
|
-
const debounceMs = options.debounce ?? 16;
|
|
26
|
-
const tabId = crypto.randomUUID();
|
|
27
|
-
const storageEventKey = `__storix_sync__:${channelName}`;
|
|
28
|
-
const queued = new Map<string, MutationPayload>();
|
|
29
|
-
const activeStoreIds = new Set<string>();
|
|
30
|
-
const messageHandlers = new Map<string, (event: MessageEvent<unknown>) => void>();
|
|
31
|
-
const storageHandlers = new Map<string, (event: StorageEvent) => void>();
|
|
32
|
-
|
|
33
|
-
let flushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
34
|
-
const channel = typeof window !== 'undefined' && 'BroadcastChannel' in window
|
|
35
|
-
? new BroadcastChannel(channelName)
|
|
36
|
-
: null;
|
|
37
|
-
|
|
38
|
-
const shouldSyncStore = (store: ScopedStore) => {
|
|
39
|
-
const ids = [store.$id, store.$scopeId];
|
|
40
|
-
if (ids.some((value) => exclude.has(value))) return false;
|
|
41
|
-
if (include.size > 0 && !ids.some((value) => include.has(value))) return false;
|
|
42
|
-
return true;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const shouldAcceptNamespace = (targetNamespace: string, sourceNamespace: string) => {
|
|
46
|
-
if (sourceNamespace === targetNamespace) return true;
|
|
47
|
-
if (allowedNamespaces === null) return true;
|
|
48
|
-
return allowedNamespaces.has(sourceNamespace);
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
const processIncomingMessage = (store: ScopedStore, payload: MutationPayload & { originTabId?: string }) => {
|
|
52
|
-
if (!payload || payload.originTabId === tabId) return;
|
|
53
|
-
if (!shouldAcceptNamespace(store.$namespace, payload.namespace)) return;
|
|
54
|
-
if (payload.storeId !== store.$id) return;
|
|
55
|
-
if (!shouldSyncStore(store)) return;
|
|
56
|
-
store.$patch({ [payload.key]: payload.newValue }, 'remote');
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const flush = () => {
|
|
60
|
-
flushTimer = null;
|
|
61
|
-
if (queued.size === 0) return;
|
|
62
|
-
|
|
63
|
-
const payloads = Array.from(queued.values());
|
|
64
|
-
queued.clear();
|
|
65
|
-
|
|
66
|
-
for (const payload of payloads) {
|
|
67
|
-
const message = {
|
|
68
|
-
...payload,
|
|
69
|
-
originTabId: tabId
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
if (channel) {
|
|
73
|
-
channel.postMessage(message);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (typeof window !== 'undefined' && 'localStorage' in window) {
|
|
77
|
-
try {
|
|
78
|
-
window.localStorage.setItem(storageEventKey, JSON.stringify(message));
|
|
79
|
-
window.localStorage.removeItem(storageEventKey);
|
|
80
|
-
} catch {
|
|
81
|
-
// Ignore quota/security exceptions.
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const queue = (payload: MutationPayload) => {
|
|
88
|
-
queued.set(`${payload.scopeId}:${payload.key}`, payload);
|
|
89
|
-
|
|
90
|
-
if (debounceMs <= 0) {
|
|
91
|
-
flush();
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (!flushTimer) {
|
|
96
|
-
flushTimer = setTimeout(flush, debounceMs);
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
return definePlugin({
|
|
101
|
-
name: 'sync',
|
|
102
|
-
onInit(store) {
|
|
103
|
-
const scopedStore = store as ScopedStore;
|
|
104
|
-
if (!shouldSyncStore(scopedStore)) return;
|
|
105
|
-
activeStoreIds.add(scopedStore.$scopeId);
|
|
106
|
-
|
|
107
|
-
if (channel) {
|
|
108
|
-
const messageHandler = (event: MessageEvent<unknown>) => {
|
|
109
|
-
processIncomingMessage(scopedStore, event.data as MutationPayload & { originTabId?: string });
|
|
110
|
-
};
|
|
111
|
-
messageHandlers.set(scopedStore.$scopeId, messageHandler);
|
|
112
|
-
channel.addEventListener('message', messageHandler);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (typeof window !== 'undefined') {
|
|
116
|
-
const storageHandler = (event: StorageEvent) => {
|
|
117
|
-
if (event.key !== storageEventKey || !event.newValue) return;
|
|
118
|
-
try {
|
|
119
|
-
const payload = JSON.parse(event.newValue) as MutationPayload & { originTabId?: string };
|
|
120
|
-
processIncomingMessage(scopedStore, payload);
|
|
121
|
-
} catch {
|
|
122
|
-
// Ignore malformed payloads.
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
storageHandlers.set(scopedStore.$scopeId, storageHandler);
|
|
126
|
-
window.addEventListener('storage', storageHandler);
|
|
127
|
-
}
|
|
128
|
-
},
|
|
129
|
-
onMutation(store, mutation) {
|
|
130
|
-
if (mutation.origin === 'remote') return;
|
|
131
|
-
if (!shouldSyncStore(store as ScopedStore)) return;
|
|
132
|
-
queue(mutation);
|
|
133
|
-
},
|
|
134
|
-
onDispose(store) {
|
|
135
|
-
const scopedStore = store as ScopedStore;
|
|
136
|
-
activeStoreIds.delete(scopedStore.$scopeId);
|
|
137
|
-
|
|
138
|
-
const messageHandler = messageHandlers.get(scopedStore.$scopeId);
|
|
139
|
-
if (channel && messageHandler) {
|
|
140
|
-
channel.removeEventListener('message', messageHandler);
|
|
141
|
-
messageHandlers.delete(scopedStore.$scopeId);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const storageHandler = storageHandlers.get(scopedStore.$scopeId);
|
|
145
|
-
if (typeof window !== 'undefined' && storageHandler) {
|
|
146
|
-
window.removeEventListener('storage', storageHandler);
|
|
147
|
-
storageHandlers.delete(scopedStore.$scopeId);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (flushTimer) {
|
|
151
|
-
clearTimeout(flushTimer);
|
|
152
|
-
flushTimer = null;
|
|
153
|
-
}
|
|
154
|
-
flush();
|
|
155
|
-
|
|
156
|
-
if (activeStoreIds.size === 0) {
|
|
157
|
-
channel?.close();
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
}
|
package/tsconfig.build.json
DELETED
package/tsconfig.json
DELETED
package/vite.config.ts
DELETED