@storix-js/sync 0.1.0 → 0.1.2

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 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/dist/index.d.ts CHANGED
@@ -3,8 +3,7 @@ export interface SyncPluginOptions {
3
3
  include?: string[];
4
4
  exclude?: string[];
5
5
  namespaces?: string[] | '*';
6
- conflict?: 'last-write-wins';
7
6
  debounce?: number;
8
7
  }
9
- export declare function syncPlugin(options?: SyncPluginOptions): import("@storix-js/core").StorePlugin;
8
+ export declare function syncPlugin(options?: SyncPluginOptions): any;
10
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,iBAAiB;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC;IAC5B,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AASD,wBAAgB,UAAU,CAAC,OAAO,GAAE,iBAAsB,yCA8IzD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,iBAAiB;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AASD,wBAAgB,UAAU,CAAC,OAAO,GAAE,iBAAsB,OA8IzD"}
package/package.json CHANGED
@@ -1,10 +1,17 @@
1
1
  {
2
2
  "name": "@storix-js/sync",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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",
@@ -15,10 +22,10 @@
15
22
  "scripts": {
16
23
  "build": "vite build && tsc -p tsconfig.build.json --emitDeclarationOnly",
17
24
  "dev": "vite build --watch",
18
- "test": "vitest run",
25
+ "test": "vitest run --passWithNoTests",
19
26
  "typecheck": "tsc -p tsconfig.json --noEmit"
20
27
  },
21
28
  "dependencies": {
22
- "@storix-js/core": "workspace:*"
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
- }
@@ -1,8 +0,0 @@
1
- {
2
- "extends": "./tsconfig.json",
3
- "compilerOptions": {
4
- "rootDir": "src",
5
- "outDir": "dist",
6
- "paths": {}
7
- }
8
- }
package/tsconfig.json DELETED
@@ -1,4 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "include": ["src"]
4
- }
package/vite.config.ts DELETED
@@ -1,15 +0,0 @@
1
- import { defineConfig } from 'vite';
2
-
3
- export default defineConfig({
4
- build: {
5
- lib: {
6
- entry: 'src/index.ts',
7
- name: 'StorixSync',
8
- fileName: 'index',
9
- formats: ['es', 'cjs']
10
- }
11
- },
12
- test: {
13
- environment: 'node'
14
- }
15
- });