@jumentix/cana-react 0.1.0
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/LICENSE.md +21 -0
- package/README.md +38 -0
- package/README.pt-BR.md +39 -0
- package/dist/collection.d.ts +9 -0
- package/dist/collection.d.ts.map +1 -0
- package/dist/collection.js +38 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +124 -0
- package/dist/index.mjs +172 -0
- package/dist/redux.d.ts +15 -0
- package/dist/redux.d.ts.map +1 -0
- package/dist/redux.js +30 -0
- package/dist/redux.mjs +28 -0
- package/package.json +71 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) XpertMinds
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# @jumentix/cana-react
|
|
2
|
+
|
|
3
|
+
React hooks and Redux-friendly helpers for `@jumentix/cana`.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @jumentix/cana @jumentix/cana-react react
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Hooks
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { useCanaLiveQuery } from '@jumentix/cana-react';
|
|
15
|
+
|
|
16
|
+
const { records, status, reload } = useCanaLiveQuery<Task>({
|
|
17
|
+
client,
|
|
18
|
+
store: 'tasks',
|
|
19
|
+
query: { index: 'byCategory', equals: categoryId },
|
|
20
|
+
getKey: (task) => task.id
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`useCanaLiveQuery()` loads a Cana table and keeps it updated from committed
|
|
25
|
+
`CanaChangeEvent` entries. When a query is present it reloads the table on
|
|
26
|
+
matching events, so filtered views stay correct.
|
|
27
|
+
|
|
28
|
+
## Redux bridge
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { connectCanaToRedux } from '@jumentix/cana-react/redux';
|
|
32
|
+
|
|
33
|
+
const bridge = connectCanaToRedux({
|
|
34
|
+
client,
|
|
35
|
+
dispatch: store.dispatch,
|
|
36
|
+
mapEvent: (event) => ({ type: 'cana/eventCommitted', payload: event })
|
|
37
|
+
});
|
|
38
|
+
```
|
package/README.pt-BR.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# @jumentix/cana-react
|
|
2
|
+
|
|
3
|
+
Hooks React e helpers para Redux sobre `@jumentix/cana`.
|
|
4
|
+
|
|
5
|
+
## Instalação
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @jumentix/cana @jumentix/cana-react react
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Hooks
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { useCanaLiveQuery } from '@jumentix/cana-react';
|
|
15
|
+
|
|
16
|
+
const { records, status, reload } = useCanaLiveQuery<Task>({
|
|
17
|
+
client,
|
|
18
|
+
store: 'tasks',
|
|
19
|
+
query: { index: 'byCategory', equals: categoryId },
|
|
20
|
+
getKey: (task) => task.id
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`useCanaLiveQuery()` carrega uma tabela Cana e mantém o estado atualizado a
|
|
25
|
+
partir de entradas `CanaChangeEvent` confirmadas. Quando há uma query, o hook
|
|
26
|
+
recarrega a tabela em eventos compatíveis, então visões filtradas continuam
|
|
27
|
+
corretas.
|
|
28
|
+
|
|
29
|
+
## Bridge Redux
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { connectCanaToRedux } from '@jumentix/cana-react/redux';
|
|
33
|
+
|
|
34
|
+
const bridge = connectCanaToRedux({
|
|
35
|
+
client,
|
|
36
|
+
dispatch: store.dispatch,
|
|
37
|
+
mapEvent: (event) => ({ type: 'cana/eventCommitted', payload: event })
|
|
38
|
+
});
|
|
39
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CanaChangeEvent, CanaKey } from '@jumentix/cana';
|
|
2
|
+
export type CanaRecordKeyReader<TRecord> = (record: TRecord) => CanaKey | undefined;
|
|
3
|
+
export type CanaCollectionOptions<TRecord> = {
|
|
4
|
+
store: string;
|
|
5
|
+
getKey?: CanaRecordKeyReader<TRecord>;
|
|
6
|
+
sort?: (a: TRecord, b: TRecord) => number;
|
|
7
|
+
};
|
|
8
|
+
export declare function applyCanaEventToRecords<TRecord>(records: readonly TRecord[], event: CanaChangeEvent, options: CanaCollectionOptions<TRecord>): TRecord[];
|
|
9
|
+
//# sourceMappingURL=collection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collection.d.ts","sourceRoot":"","sources":["../src/collection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAE/D,MAAM,MAAM,mBAAmB,CAAC,OAAO,IAAI,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,GAAG,SAAS,CAAC;AAEpF,MAAM,MAAM,qBAAqB,CAAC,OAAO,IAAI;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,mBAAmB,CAAC,OAAO,CAAC,CAAC;IACtC,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,KAAK,MAAM,CAAC;CAC3C,CAAC;AAuBF,wBAAgB,uBAAuB,CAAC,OAAO,EAC7C,OAAO,EAAE,SAAS,OAAO,EAAE,EAC3B,KAAK,EAAE,eAAe,EACtB,OAAO,EAAE,qBAAqB,CAAC,OAAO,CAAC,GACtC,OAAO,EAAE,CAgBX"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.applyCanaEventToRecords = void 0;
|
|
4
|
+
function defaultGetKey(record) {
|
|
5
|
+
if (record && typeof record === 'object' && 'id' in record) {
|
|
6
|
+
return record.id;
|
|
7
|
+
}
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
function sameKey(a, b) {
|
|
11
|
+
if (a instanceof Date && b instanceof Date)
|
|
12
|
+
return a.getTime() === b.getTime();
|
|
13
|
+
return Object.is(a, b);
|
|
14
|
+
}
|
|
15
|
+
function sorted(records, sort) {
|
|
16
|
+
const next = [...records];
|
|
17
|
+
if (sort)
|
|
18
|
+
next.sort(sort);
|
|
19
|
+
return next;
|
|
20
|
+
}
|
|
21
|
+
function applyCanaEventToRecords(records, event, options) {
|
|
22
|
+
if (event.store !== options.store)
|
|
23
|
+
return [...records];
|
|
24
|
+
if (event.type === 'cleared')
|
|
25
|
+
return [];
|
|
26
|
+
const getKey = options.getKey ?? defaultGetKey;
|
|
27
|
+
if (event.type === 'deleted') {
|
|
28
|
+
return records.filter((record) => !sameKey(getKey(record), event.key));
|
|
29
|
+
}
|
|
30
|
+
if (!event.record)
|
|
31
|
+
return [...records];
|
|
32
|
+
const record = event.record;
|
|
33
|
+
const recordKey = getKey(record);
|
|
34
|
+
const next = records.filter((item) => !sameKey(getKey(item), recordKey));
|
|
35
|
+
next.push(record);
|
|
36
|
+
return sorted(next, options.sort);
|
|
37
|
+
}
|
|
38
|
+
exports.applyCanaEventToRecords = applyCanaEventToRecords;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { CanaChangeEvent, CanaClient, CanaQuery } from '@jumentix/cana';
|
|
2
|
+
import { applyCanaEventToRecords, type CanaCollectionOptions, type CanaRecordKeyReader } from './collection';
|
|
3
|
+
export type { CanaCollectionOptions, CanaRecordKeyReader };
|
|
4
|
+
export { applyCanaEventToRecords };
|
|
5
|
+
export type CanaHookStatus = 'idle' | 'opening' | 'loading' | 'ready' | 'error';
|
|
6
|
+
export type UseCanaClientResult<TClient extends CanaClient> = {
|
|
7
|
+
client: TClient | null;
|
|
8
|
+
status: CanaHookStatus;
|
|
9
|
+
error: unknown;
|
|
10
|
+
};
|
|
11
|
+
export declare function useCanaClient<TClient extends CanaClient>(create: () => TClient | Promise<TClient>, deps?: readonly unknown[]): UseCanaClientResult<TClient>;
|
|
12
|
+
export type UseCanaSubscriptionOptions = {
|
|
13
|
+
enabled?: boolean;
|
|
14
|
+
sinceCursor?: number;
|
|
15
|
+
onError?: (error: unknown) => void;
|
|
16
|
+
};
|
|
17
|
+
export declare function useCanaSubscription(client: CanaClient | null | undefined, listener: (event: CanaChangeEvent) => void, options?: UseCanaSubscriptionOptions): void;
|
|
18
|
+
export type UseCanaLiveQueryOptions<TRecord> = {
|
|
19
|
+
client: CanaClient | null | undefined;
|
|
20
|
+
store: string;
|
|
21
|
+
query?: CanaQuery;
|
|
22
|
+
enabled?: boolean;
|
|
23
|
+
getKey?: CanaRecordKeyReader<TRecord>;
|
|
24
|
+
sort?: (a: TRecord, b: TRecord) => number;
|
|
25
|
+
reloadOnEvent?: boolean;
|
|
26
|
+
onError?: (error: unknown) => void;
|
|
27
|
+
};
|
|
28
|
+
export type UseCanaLiveQueryResult<TRecord> = {
|
|
29
|
+
records: TRecord[];
|
|
30
|
+
status: CanaHookStatus;
|
|
31
|
+
error: unknown;
|
|
32
|
+
reload: () => Promise<TRecord[]>;
|
|
33
|
+
};
|
|
34
|
+
export declare function useCanaLiveQuery<TRecord>(options: UseCanaLiveQueryOptions<TRecord>): UseCanaLiveQueryResult<TRecord>;
|
|
35
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,eAAe,EACf,UAAU,EACV,SAAS,EACV,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,uBAAuB,EACvB,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACzB,MAAM,cAAc,CAAC;AAEtB,YAAY,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,CAAC;AAC3D,OAAO,EAAE,uBAAuB,EAAE,CAAC;AAEnC,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,CAAC;AAEhF,MAAM,MAAM,mBAAmB,CAAC,OAAO,SAAS,UAAU,IAAI;IAC5D,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;IACvB,MAAM,EAAE,cAAc,CAAC;IACvB,KAAK,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,wBAAgB,aAAa,CAAC,OAAO,SAAS,UAAU,EACtD,MAAM,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,EACxC,IAAI,GAAE,SAAS,OAAO,EAAO,GAC5B,mBAAmB,CAAC,OAAO,CAAC,CAuC9B;AAED,MAAM,MAAM,0BAA0B,GAAG;IACvC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC,CAAC;AAEF,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,EACrC,QAAQ,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,EAC1C,OAAO,GAAE,0BAA+B,GACvC,IAAI,CAgBN;AAED,MAAM,MAAM,uBAAuB,CAAC,OAAO,IAAI;IAC7C,MAAM,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,CAAC;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,mBAAmB,CAAC,OAAO,CAAC,CAAC;IACtC,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,KAAK,MAAM,CAAC;IAC1C,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,sBAAsB,CAAC,OAAO,IAAI;IAC5C,OAAO,EAAE,OAAO,EAAE,CAAC;IACnB,MAAM,EAAE,cAAc,CAAC;IACvB,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;CAClC,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,OAAO,EACtC,OAAO,EAAE,uBAAuB,CAAC,OAAO,CAAC,GACxC,sBAAsB,CAAC,OAAO,CAAC,CAqEjC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useCanaLiveQuery = exports.useCanaSubscription = exports.useCanaClient = exports.applyCanaEventToRecords = void 0;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
const collection_1 = require("./collection");
|
|
6
|
+
Object.defineProperty(exports, "applyCanaEventToRecords", { enumerable: true, get: function () { return collection_1.applyCanaEventToRecords; } });
|
|
7
|
+
function useCanaClient(create, deps = []) {
|
|
8
|
+
const [client, setClient] = (0, react_1.useState)(null);
|
|
9
|
+
const [status, setStatus] = (0, react_1.useState)('idle');
|
|
10
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
11
|
+
(0, react_1.useEffect)(() => {
|
|
12
|
+
let alive = true;
|
|
13
|
+
let opened = null;
|
|
14
|
+
setStatus('opening');
|
|
15
|
+
setError(null);
|
|
16
|
+
Promise.resolve(create())
|
|
17
|
+
.then(async (next) => {
|
|
18
|
+
opened = next;
|
|
19
|
+
await next.open();
|
|
20
|
+
if (!alive) {
|
|
21
|
+
await next.close();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
setClient(next);
|
|
25
|
+
setStatus('ready');
|
|
26
|
+
})
|
|
27
|
+
.catch((err) => {
|
|
28
|
+
if (!alive)
|
|
29
|
+
return;
|
|
30
|
+
setError(err);
|
|
31
|
+
setStatus('error');
|
|
32
|
+
});
|
|
33
|
+
return () => {
|
|
34
|
+
alive = false;
|
|
35
|
+
setClient(null);
|
|
36
|
+
if (opened) {
|
|
37
|
+
opened.close().catch(() => undefined);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}, deps);
|
|
41
|
+
return { client, status, error };
|
|
42
|
+
}
|
|
43
|
+
exports.useCanaClient = useCanaClient;
|
|
44
|
+
function useCanaSubscription(client, listener, options = {}) {
|
|
45
|
+
const listenerRef = (0, react_1.useRef)(listener);
|
|
46
|
+
listenerRef.current = listener;
|
|
47
|
+
(0, react_1.useEffect)(() => {
|
|
48
|
+
if (!client || options.enabled === false)
|
|
49
|
+
return undefined;
|
|
50
|
+
try {
|
|
51
|
+
return client.subscribe((event) => listenerRef.current(event), options.sinceCursor === undefined ? undefined : { sinceCursor: options.sinceCursor });
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
options.onError?.(error);
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
}, [client, options.enabled, options.sinceCursor, options.onError]);
|
|
58
|
+
}
|
|
59
|
+
exports.useCanaSubscription = useCanaSubscription;
|
|
60
|
+
function useCanaLiveQuery(options) {
|
|
61
|
+
const { client, store, enabled = true, getKey, sort, onError } = options;
|
|
62
|
+
const queryKey = JSON.stringify(options.query ?? null);
|
|
63
|
+
const query = (0, react_1.useMemo)(() => options.query, [queryKey]);
|
|
64
|
+
const reloadOnEvent = options.reloadOnEvent ?? Boolean(options.query);
|
|
65
|
+
const [records, setRecords] = (0, react_1.useState)([]);
|
|
66
|
+
const [status, setStatus] = (0, react_1.useState)('idle');
|
|
67
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
68
|
+
const reload = (0, react_1.useCallback)(async () => {
|
|
69
|
+
if (!client || !enabled)
|
|
70
|
+
return [];
|
|
71
|
+
setStatus('loading');
|
|
72
|
+
setError(null);
|
|
73
|
+
try {
|
|
74
|
+
const next = [...await client.table(store).query(query)];
|
|
75
|
+
if (sort)
|
|
76
|
+
next.sort(sort);
|
|
77
|
+
setRecords(next);
|
|
78
|
+
setStatus('ready');
|
|
79
|
+
return next;
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
setError(err);
|
|
83
|
+
setStatus('error');
|
|
84
|
+
onError?.(err);
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
}, [client, enabled, onError, query, sort, store]);
|
|
88
|
+
(0, react_1.useEffect)(() => {
|
|
89
|
+
if (!client || !enabled)
|
|
90
|
+
return undefined;
|
|
91
|
+
let alive = true;
|
|
92
|
+
const safeReload = async () => {
|
|
93
|
+
if (!alive)
|
|
94
|
+
return;
|
|
95
|
+
await reload();
|
|
96
|
+
};
|
|
97
|
+
safeReload().catch((err) => onError?.(err));
|
|
98
|
+
const stop = client.subscribe((event) => {
|
|
99
|
+
if (event.store !== store)
|
|
100
|
+
return;
|
|
101
|
+
if (reloadOnEvent) {
|
|
102
|
+
safeReload().catch((err) => onError?.(err));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const collectionOptions = {
|
|
106
|
+
store,
|
|
107
|
+
getKey,
|
|
108
|
+
sort
|
|
109
|
+
};
|
|
110
|
+
setRecords((current) => (0, collection_1.applyCanaEventToRecords)(current, event, collectionOptions));
|
|
111
|
+
});
|
|
112
|
+
return () => {
|
|
113
|
+
alive = false;
|
|
114
|
+
stop();
|
|
115
|
+
};
|
|
116
|
+
}, [client, enabled, getKey, reload, reloadOnEvent, sort, store]);
|
|
117
|
+
return {
|
|
118
|
+
records,
|
|
119
|
+
status,
|
|
120
|
+
error,
|
|
121
|
+
reload
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
exports.useCanaLiveQuery = useCanaLiveQuery;
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import {
|
|
3
|
+
useCallback,
|
|
4
|
+
useEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
useState
|
|
8
|
+
} from "react";
|
|
9
|
+
|
|
10
|
+
// src/collection.ts
|
|
11
|
+
function defaultGetKey(record) {
|
|
12
|
+
if (record && typeof record === "object" && "id" in record) {
|
|
13
|
+
return record.id;
|
|
14
|
+
}
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
function sameKey(a, b) {
|
|
18
|
+
if (a instanceof Date && b instanceof Date)
|
|
19
|
+
return a.getTime() === b.getTime();
|
|
20
|
+
return Object.is(a, b);
|
|
21
|
+
}
|
|
22
|
+
function sorted(records, sort) {
|
|
23
|
+
const next = [...records];
|
|
24
|
+
if (sort)
|
|
25
|
+
next.sort(sort);
|
|
26
|
+
return next;
|
|
27
|
+
}
|
|
28
|
+
function applyCanaEventToRecords(records, event, options) {
|
|
29
|
+
if (event.store !== options.store)
|
|
30
|
+
return [...records];
|
|
31
|
+
if (event.type === "cleared")
|
|
32
|
+
return [];
|
|
33
|
+
const getKey = options.getKey ?? defaultGetKey;
|
|
34
|
+
if (event.type === "deleted") {
|
|
35
|
+
return records.filter((record2) => !sameKey(getKey(record2), event.key));
|
|
36
|
+
}
|
|
37
|
+
if (!event.record)
|
|
38
|
+
return [...records];
|
|
39
|
+
const record = event.record;
|
|
40
|
+
const recordKey = getKey(record);
|
|
41
|
+
const next = records.filter((item) => !sameKey(getKey(item), recordKey));
|
|
42
|
+
next.push(record);
|
|
43
|
+
return sorted(next, options.sort);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/index.ts
|
|
47
|
+
function useCanaClient(create, deps = []) {
|
|
48
|
+
const [client, setClient] = useState(null);
|
|
49
|
+
const [status, setStatus] = useState("idle");
|
|
50
|
+
const [error, setError] = useState(null);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
let alive = true;
|
|
53
|
+
let opened = null;
|
|
54
|
+
setStatus("opening");
|
|
55
|
+
setError(null);
|
|
56
|
+
Promise.resolve(create()).then(async (next) => {
|
|
57
|
+
opened = next;
|
|
58
|
+
await next.open();
|
|
59
|
+
if (!alive) {
|
|
60
|
+
await next.close();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
setClient(next);
|
|
64
|
+
setStatus("ready");
|
|
65
|
+
}).catch((err) => {
|
|
66
|
+
if (!alive)
|
|
67
|
+
return;
|
|
68
|
+
setError(err);
|
|
69
|
+
setStatus("error");
|
|
70
|
+
});
|
|
71
|
+
return () => {
|
|
72
|
+
alive = false;
|
|
73
|
+
setClient(null);
|
|
74
|
+
if (opened) {
|
|
75
|
+
opened.close().catch(() => {
|
|
76
|
+
return;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}, deps);
|
|
81
|
+
return { client, status, error };
|
|
82
|
+
}
|
|
83
|
+
function useCanaSubscription(client, listener, options = {}) {
|
|
84
|
+
const listenerRef = useRef(listener);
|
|
85
|
+
listenerRef.current = listener;
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (!client || options.enabled === false)
|
|
88
|
+
return;
|
|
89
|
+
try {
|
|
90
|
+
return client.subscribe((event) => listenerRef.current(event), options.sinceCursor === undefined ? undefined : { sinceCursor: options.sinceCursor });
|
|
91
|
+
} catch (error) {
|
|
92
|
+
options.onError?.(error);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
}, [client, options.enabled, options.sinceCursor, options.onError]);
|
|
96
|
+
}
|
|
97
|
+
function useCanaLiveQuery(options) {
|
|
98
|
+
const {
|
|
99
|
+
client,
|
|
100
|
+
store,
|
|
101
|
+
enabled = true,
|
|
102
|
+
getKey,
|
|
103
|
+
sort,
|
|
104
|
+
onError
|
|
105
|
+
} = options;
|
|
106
|
+
const queryKey = JSON.stringify(options.query ?? null);
|
|
107
|
+
const query = useMemo(() => options.query, [queryKey]);
|
|
108
|
+
const reloadOnEvent = options.reloadOnEvent ?? Boolean(options.query);
|
|
109
|
+
const [records, setRecords] = useState([]);
|
|
110
|
+
const [status, setStatus] = useState("idle");
|
|
111
|
+
const [error, setError] = useState(null);
|
|
112
|
+
const reload = useCallback(async () => {
|
|
113
|
+
if (!client || !enabled)
|
|
114
|
+
return [];
|
|
115
|
+
setStatus("loading");
|
|
116
|
+
setError(null);
|
|
117
|
+
try {
|
|
118
|
+
const next = [...await client.table(store).query(query)];
|
|
119
|
+
if (sort)
|
|
120
|
+
next.sort(sort);
|
|
121
|
+
setRecords(next);
|
|
122
|
+
setStatus("ready");
|
|
123
|
+
return next;
|
|
124
|
+
} catch (err) {
|
|
125
|
+
setError(err);
|
|
126
|
+
setStatus("error");
|
|
127
|
+
onError?.(err);
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
}, [client, enabled, onError, query, sort, store]);
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
if (!client || !enabled)
|
|
133
|
+
return;
|
|
134
|
+
let alive = true;
|
|
135
|
+
const safeReload = async () => {
|
|
136
|
+
if (!alive)
|
|
137
|
+
return;
|
|
138
|
+
await reload();
|
|
139
|
+
};
|
|
140
|
+
safeReload().catch((err) => onError?.(err));
|
|
141
|
+
const stop = client.subscribe((event) => {
|
|
142
|
+
if (event.store !== store)
|
|
143
|
+
return;
|
|
144
|
+
if (reloadOnEvent) {
|
|
145
|
+
safeReload().catch((err) => onError?.(err));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const collectionOptions = {
|
|
149
|
+
store,
|
|
150
|
+
getKey,
|
|
151
|
+
sort
|
|
152
|
+
};
|
|
153
|
+
setRecords((current) => applyCanaEventToRecords(current, event, collectionOptions));
|
|
154
|
+
});
|
|
155
|
+
return () => {
|
|
156
|
+
alive = false;
|
|
157
|
+
stop();
|
|
158
|
+
};
|
|
159
|
+
}, [client, enabled, getKey, reload, reloadOnEvent, sort, store]);
|
|
160
|
+
return {
|
|
161
|
+
records,
|
|
162
|
+
status,
|
|
163
|
+
error,
|
|
164
|
+
reload
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
export {
|
|
168
|
+
useCanaSubscription,
|
|
169
|
+
useCanaLiveQuery,
|
|
170
|
+
useCanaClient,
|
|
171
|
+
applyCanaEventToRecords
|
|
172
|
+
};
|
package/dist/redux.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CanaChangeEvent, CanaClient } from '@jumentix/cana';
|
|
2
|
+
export type CanaReduxDispatch<TAction> = (action: TAction) => unknown;
|
|
3
|
+
export type CanaReduxBridge = {
|
|
4
|
+
stop: () => void;
|
|
5
|
+
getLastCursor: () => number;
|
|
6
|
+
};
|
|
7
|
+
export type ConnectCanaToReduxOptions<TAction> = {
|
|
8
|
+
client: CanaClient;
|
|
9
|
+
dispatch: CanaReduxDispatch<TAction>;
|
|
10
|
+
mapEvent: (event: CanaChangeEvent) => TAction | readonly TAction[] | undefined;
|
|
11
|
+
sinceCursor?: number;
|
|
12
|
+
onError?: (error: unknown) => void;
|
|
13
|
+
};
|
|
14
|
+
export declare function connectCanaToRedux<TAction>(options: ConnectCanaToReduxOptions<TAction>): CanaReduxBridge;
|
|
15
|
+
//# sourceMappingURL=redux.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redux.d.ts","sourceRoot":"","sources":["../src/redux.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAElE,MAAM,MAAM,iBAAiB,CAAC,OAAO,IAAI,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC;AAEtE,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,aAAa,EAAE,MAAM,MAAM,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,yBAAyB,CAAC,OAAO,IAAI;IAC/C,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,EAAE,iBAAiB,CAAC,OAAO,CAAC,CAAC;IACrC,QAAQ,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,OAAO,GAAG,SAAS,OAAO,EAAE,GAAG,SAAS,CAAC;IAC/E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,OAAO,EACxC,OAAO,EAAE,yBAAyB,CAAC,OAAO,CAAC,GAC1C,eAAe,CAyBjB"}
|
package/dist/redux.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.connectCanaToRedux = void 0;
|
|
4
|
+
function connectCanaToRedux(options) {
|
|
5
|
+
let lastCursor = options.sinceCursor ?? 0;
|
|
6
|
+
let stop = () => { };
|
|
7
|
+
try {
|
|
8
|
+
stop = options.client.subscribe((event) => {
|
|
9
|
+
const mapped = options.mapEvent(event);
|
|
10
|
+
if (Array.isArray(mapped)) {
|
|
11
|
+
for (const action of mapped) {
|
|
12
|
+
options.dispatch(action);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
else if (mapped !== undefined) {
|
|
16
|
+
options.dispatch(mapped);
|
|
17
|
+
}
|
|
18
|
+
lastCursor = event.cursor;
|
|
19
|
+
}, options.sinceCursor === undefined ? undefined : { sinceCursor: options.sinceCursor });
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
options.onError?.(error);
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
stop,
|
|
27
|
+
getLastCursor: () => lastCursor
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
exports.connectCanaToRedux = connectCanaToRedux;
|
package/dist/redux.mjs
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// src/redux.ts
|
|
2
|
+
function connectCanaToRedux(options) {
|
|
3
|
+
let lastCursor = options.sinceCursor ?? 0;
|
|
4
|
+
let stop = () => {};
|
|
5
|
+
try {
|
|
6
|
+
stop = options.client.subscribe((event) => {
|
|
7
|
+
const mapped = options.mapEvent(event);
|
|
8
|
+
if (Array.isArray(mapped)) {
|
|
9
|
+
for (const action of mapped) {
|
|
10
|
+
options.dispatch(action);
|
|
11
|
+
}
|
|
12
|
+
} else if (mapped !== undefined) {
|
|
13
|
+
options.dispatch(mapped);
|
|
14
|
+
}
|
|
15
|
+
lastCursor = event.cursor;
|
|
16
|
+
}, options.sinceCursor === undefined ? undefined : { sinceCursor: options.sinceCursor });
|
|
17
|
+
} catch (error) {
|
|
18
|
+
options.onError?.(error);
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
stop,
|
|
23
|
+
getLastCursor: () => lastCursor
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export {
|
|
27
|
+
connectCanaToRedux
|
|
28
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jumentix/cana-react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React hooks and Redux-friendly helpers for @jumentix/cana.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"module": "dist/index.mjs",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.mjs",
|
|
13
|
+
"require": "./dist/index.js",
|
|
14
|
+
"default": "./dist/index.mjs"
|
|
15
|
+
},
|
|
16
|
+
"./redux": {
|
|
17
|
+
"types": "./dist/redux.d.ts",
|
|
18
|
+
"import": "./dist/redux.mjs",
|
|
19
|
+
"require": "./dist/redux.js",
|
|
20
|
+
"default": "./dist/redux.mjs"
|
|
21
|
+
},
|
|
22
|
+
"./package.json": "./package.json"
|
|
23
|
+
},
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"README.md",
|
|
28
|
+
"README.pt-BR.md",
|
|
29
|
+
"LICENSE.md"
|
|
30
|
+
],
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@jumentix/cana": "^0.1.0",
|
|
33
|
+
"react": ">=18.0.0 || >=19.0.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@jumentix/cana": "0.1.0",
|
|
37
|
+
"@types/react": "19.2.18",
|
|
38
|
+
"@types/react-test-renderer": "19.3.0",
|
|
39
|
+
"react": "19.2.8",
|
|
40
|
+
"react-test-renderer": "19.3.0"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsc -p tsconfig.json --noEmit false && bun run build:esm",
|
|
44
|
+
"clean": "rm -rf dist",
|
|
45
|
+
"build:esm": "bun build src/index.ts --outfile dist/index.mjs --format esm --target browser --external react --external @jumentix/cana && bun build src/redux.ts --outfile dist/redux.mjs --format esm --target browser --external @jumentix/cana",
|
|
46
|
+
"test": "bun test --isolate test",
|
|
47
|
+
"lint": "echo \"cana-react: covered by the root eslint run\"",
|
|
48
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
49
|
+
"prepublishOnly": "bun run clean && bun run build"
|
|
50
|
+
},
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public"
|
|
53
|
+
},
|
|
54
|
+
"repository": {
|
|
55
|
+
"type": "git",
|
|
56
|
+
"url": "https://github.com/web2solutions/Jumentix.git",
|
|
57
|
+
"directory": "packages/cana-react"
|
|
58
|
+
},
|
|
59
|
+
"homepage": "https://jumentix-website.vercel.app",
|
|
60
|
+
"bugs": {
|
|
61
|
+
"url": "https://github.com/web2solutions/Jumentix/issues"
|
|
62
|
+
},
|
|
63
|
+
"keywords": [
|
|
64
|
+
"indexeddb",
|
|
65
|
+
"offline-first",
|
|
66
|
+
"react",
|
|
67
|
+
"redux",
|
|
68
|
+
"jumentix",
|
|
69
|
+
"cana"
|
|
70
|
+
]
|
|
71
|
+
}
|