@anfenn/zync 0.1.11 → 0.1.12
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 +122 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,126 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Zync
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@anfenn/zync)
|
|
4
|
+
|
|
5
|
+
Unopinionated, bullet-proof, offline-first sync middleware for Zustand.
|
|
6
|
+
|
|
7
|
+
## Benefits
|
|
8
|
+
|
|
9
|
+
- Uses the official persist middleware as the local storage (localStorage, IndexedDB, etc.)
|
|
10
|
+
- Zync's persistWithSync() is a drop-in replacement for Zustand's persist()
|
|
11
|
+
- Allows for idiomatic use of Zustand
|
|
12
|
+
- Leaves the api requests up to you (RESTful, GraphQL, etc.), just provide add(), update(), remove() and list()
|
|
13
|
+
|
|
14
|
+
## Requirements
|
|
15
|
+
|
|
16
|
+
- Client records will have a `_localId` field which is stable and never sent to the server. It is ideal for use as a key in JSX. The provided helper function `nextLocalId()` returns a UUID, but you could use any unique value
|
|
17
|
+
- Server records must have:
|
|
18
|
+
- `id`: Server assigned unique identifier (any datatype)
|
|
19
|
+
- `updated_at`: Server assigned (trigger or api layer). The client will never send this as the client clock is unlikely to be in sync with the server, so is never used for change detection
|
|
20
|
+
- `deleted`: Boolean, used for soft deletes, to allow other clients to download deleted records to keep their local records in sync
|
|
21
|
+
|
|
22
|
+
## Quickstart
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install @anfenn/zync
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Zustand store creation:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { SyncAction, UseStoreWithSync, persistWithSync } from '@anfenn/zync';
|
|
32
|
+
import { create } from 'zustand';
|
|
33
|
+
import { createJSONStorage } from 'zustand/middleware';
|
|
34
|
+
import { useShallow } from 'zustand/react/shallow';
|
|
35
|
+
|
|
36
|
+
type Store = {
|
|
37
|
+
facts: Fact[];
|
|
38
|
+
addFact: (item: Fact) => void;
|
|
39
|
+
updateFact: (localId: string, changes: Partial<Fact>) => void;
|
|
40
|
+
removeFact: (localId: string) => void;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const useStore = create<any>()(
|
|
44
|
+
persistWithSync<Store>(
|
|
45
|
+
(set, get, queueToSync) => ({
|
|
46
|
+
// Standard Zustand state and mutation functions with new queueToSync() function
|
|
47
|
+
|
|
48
|
+
facts: [],
|
|
49
|
+
addFact: (item: Fact) => {
|
|
50
|
+
const updated_at = new Date().toISOString();
|
|
51
|
+
const newItem = { ...item, created_at: updated_at, updated_at };
|
|
52
|
+
|
|
53
|
+
set((state: Store) => ({
|
|
54
|
+
facts: [...state.facts, newItem],
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
queueToSync(SyncAction.CreateOrUpdate, item._localId, 'facts');
|
|
58
|
+
},
|
|
59
|
+
updateFact: (localId: string, changes: Partial<Fact>) => {
|
|
60
|
+
set((state: Store) => ({
|
|
61
|
+
facts: state.facts.map((item) => (item._localId === localId ? { ...item, ...changes } : item)),
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
queueToSync(SyncAction.CreateOrUpdate, localId, 'facts');
|
|
65
|
+
},
|
|
66
|
+
removeFact: (localId: string) => {
|
|
67
|
+
queueToSync(SyncAction.Remove, 'facts', localId);
|
|
68
|
+
|
|
69
|
+
set((state: Store) => ({
|
|
70
|
+
facts: state.facts.filter((item) => item._localId !== localId),
|
|
71
|
+
}));
|
|
72
|
+
},
|
|
73
|
+
}),
|
|
74
|
+
{
|
|
75
|
+
// Standard Zustand persist options
|
|
76
|
+
|
|
77
|
+
name: 'store',
|
|
78
|
+
storage: createJSONStorage(() => localStorage),
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
// State-to-API map to enable syncing. Must implement the full CRUD API:
|
|
82
|
+
//
|
|
83
|
+
// add: (item: any) => Promise<any | undefined>
|
|
84
|
+
// update: (id: any, changes: any) => Promise<boolean>
|
|
85
|
+
// remove: (id: any) => Promise<void>
|
|
86
|
+
// list: (lastUpdatedAt: Date) => Promise<any[]>
|
|
87
|
+
// firstLoad: (lastId: any) => Promise<any[]> (Optional)
|
|
88
|
+
|
|
89
|
+
facts: factApi,
|
|
90
|
+
},
|
|
91
|
+
),
|
|
92
|
+
) as UseStoreWithSync<Store>;
|
|
93
|
+
|
|
94
|
+
const useFacts = () =>
|
|
95
|
+
useStore(
|
|
96
|
+
useShallow(({ facts, addFact, updateFact, removeFact }) => ({
|
|
97
|
+
facts,
|
|
98
|
+
addFact,
|
|
99
|
+
updateFact,
|
|
100
|
+
removeFact,
|
|
101
|
+
})),
|
|
102
|
+
);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### In your component:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
// Your state
|
|
109
|
+
const { facts, addFact } = useFacts();
|
|
110
|
+
|
|
111
|
+
// Zync's internal sync state
|
|
112
|
+
const syncState = useStore((state) => state.syncState);
|
|
113
|
+
// syncState.status // 'hydrating' | 'syncing' | 'idle'
|
|
114
|
+
// syncState.error
|
|
115
|
+
// syncState.enabled
|
|
116
|
+
// syncState.firstLoadDone
|
|
117
|
+
// syncState.pendingChanges
|
|
118
|
+
// syncState.lastPulled
|
|
119
|
+
|
|
120
|
+
// Zync's control api
|
|
121
|
+
useStore.sync.enable(true | false);
|
|
122
|
+
useStore.sync.startFirstLoad();
|
|
123
|
+
```
|
|
4
124
|
|
|
5
125
|
## Optional IndexedDB storage
|
|
6
126
|
|