@entifix/react-integration 0.1.0-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 +21 -0
- package/README.md +7 -0
- package/dist/context/adapters-context/adapters-context.d.ts +4 -0
- package/dist/context/adapters-context/adapters-context.d.ts.map +1 -0
- package/dist/context/adapters-context/adapters-context.js +19 -0
- package/dist/context/adapters-context/adapters-context.js.map +1 -0
- package/dist/context/adapters-context/index.d.ts +2 -0
- package/dist/context/adapters-context/index.d.ts.map +1 -0
- package/dist/context/adapters-context/index.js +3 -0
- package/dist/context/adapters-context/index.js.map +1 -0
- package/dist/hooks/use-data-loading/index.d.ts +3 -0
- package/dist/hooks/use-data-loading/index.d.ts.map +1 -0
- package/dist/hooks/use-data-loading/index.js +4 -0
- package/dist/hooks/use-data-loading/index.js.map +1 -0
- package/dist/hooks/use-data-loading/use-data-loading.d.ts +26 -0
- package/dist/hooks/use-data-loading/use-data-loading.d.ts.map +1 -0
- package/dist/hooks/use-data-loading/use-data-loading.js +167 -0
- package/dist/hooks/use-data-loading/use-data-loading.js.map +1 -0
- package/dist/hooks/use-data-loading/use-data-loading.types.d.ts +53 -0
- package/dist/hooks/use-data-loading/use-data-loading.types.d.ts.map +1 -0
- package/dist/hooks/use-data-loading/use-data-loading.types.js +3 -0
- package/dist/hooks/use-data-loading/use-data-loading.types.js.map +1 -0
- package/dist/hooks/use-entity-form/index.d.ts +4 -0
- package/dist/hooks/use-entity-form/index.d.ts.map +1 -0
- package/dist/hooks/use-entity-form/index.js +5 -0
- package/dist/hooks/use-entity-form/index.js.map +1 -0
- package/dist/hooks/use-entity-form/use-entity-form.d.ts +31 -0
- package/dist/hooks/use-entity-form/use-entity-form.d.ts.map +1 -0
- package/dist/hooks/use-entity-form/use-entity-form.helpers.d.ts +136 -0
- package/dist/hooks/use-entity-form/use-entity-form.helpers.d.ts.map +1 -0
- package/dist/hooks/use-entity-form/use-entity-form.helpers.js +337 -0
- package/dist/hooks/use-entity-form/use-entity-form.helpers.js.map +1 -0
- package/dist/hooks/use-entity-form/use-entity-form.js +209 -0
- package/dist/hooks/use-entity-form/use-entity-form.js.map +1 -0
- package/dist/hooks/use-entity-form/use-entity-form.types.d.ts +173 -0
- package/dist/hooks/use-entity-form/use-entity-form.types.d.ts.map +1 -0
- package/dist/hooks/use-entity-form/use-entity-form.types.js +3 -0
- package/dist/hooks/use-entity-form/use-entity-form.types.js.map +1 -0
- package/dist/hooks/use-entity-link-resolver/index.d.ts +2 -0
- package/dist/hooks/use-entity-link-resolver/index.d.ts.map +1 -0
- package/dist/hooks/use-entity-link-resolver/index.js +3 -0
- package/dist/hooks/use-entity-link-resolver/index.js.map +1 -0
- package/dist/hooks/use-entity-link-resolver/use-entity-link-resolver.d.ts +17 -0
- package/dist/hooks/use-entity-link-resolver/use-entity-link-resolver.d.ts.map +1 -0
- package/dist/hooks/use-entity-link-resolver/use-entity-link-resolver.js +26 -0
- package/dist/hooks/use-entity-link-resolver/use-entity-link-resolver.js.map +1 -0
- package/dist/hooks/use-entity-link-source/index.d.ts +3 -0
- package/dist/hooks/use-entity-link-source/index.d.ts.map +1 -0
- package/dist/hooks/use-entity-link-source/index.js +4 -0
- package/dist/hooks/use-entity-link-source/index.js.map +1 -0
- package/dist/hooks/use-entity-link-source/use-entity-link-source.d.ts +25 -0
- package/dist/hooks/use-entity-link-source/use-entity-link-source.d.ts.map +1 -0
- package/dist/hooks/use-entity-link-source/use-entity-link-source.js +169 -0
- package/dist/hooks/use-entity-link-source/use-entity-link-source.js.map +1 -0
- package/dist/hooks/use-entity-link-source/use-entity-link-source.types.d.ts +49 -0
- package/dist/hooks/use-entity-link-source/use-entity-link-source.types.d.ts.map +1 -0
- package/dist/hooks/use-entity-link-source/use-entity-link-source.types.js +3 -0
- package/dist/hooks/use-entity-link-source/use-entity-link-source.types.js.map +1 -0
- package/dist/hooks/use-entity-mutation/index.d.ts +3 -0
- package/dist/hooks/use-entity-mutation/index.d.ts.map +1 -0
- package/dist/hooks/use-entity-mutation/index.js +4 -0
- package/dist/hooks/use-entity-mutation/index.js.map +1 -0
- package/dist/hooks/use-entity-mutation/use-entity-mutation.d.ts +22 -0
- package/dist/hooks/use-entity-mutation/use-entity-mutation.d.ts.map +1 -0
- package/dist/hooks/use-entity-mutation/use-entity-mutation.js +88 -0
- package/dist/hooks/use-entity-mutation/use-entity-mutation.js.map +1 -0
- package/dist/hooks/use-entity-mutation/use-entity-mutation.types.d.ts +19 -0
- package/dist/hooks/use-entity-mutation/use-entity-mutation.types.d.ts.map +1 -0
- package/dist/hooks/use-entity-mutation/use-entity-mutation.types.js +3 -0
- package/dist/hooks/use-entity-mutation/use-entity-mutation.types.js.map +1 -0
- package/dist/hooks/use-entity-record/index.d.ts +3 -0
- package/dist/hooks/use-entity-record/index.d.ts.map +1 -0
- package/dist/hooks/use-entity-record/index.js +4 -0
- package/dist/hooks/use-entity-record/index.js.map +1 -0
- package/dist/hooks/use-entity-record/use-entity-record.d.ts +21 -0
- package/dist/hooks/use-entity-record/use-entity-record.d.ts.map +1 -0
- package/dist/hooks/use-entity-record/use-entity-record.js +93 -0
- package/dist/hooks/use-entity-record/use-entity-record.js.map +1 -0
- package/dist/hooks/use-entity-record/use-entity-record.types.d.ts +22 -0
- package/dist/hooks/use-entity-record/use-entity-record.types.d.ts.map +1 -0
- package/dist/hooks/use-entity-record/use-entity-record.types.js +3 -0
- package/dist/hooks/use-entity-record/use-entity-record.types.js.map +1 -0
- package/dist/hooks/use-entity-use-cases/index.d.ts +2 -0
- package/dist/hooks/use-entity-use-cases/index.d.ts.map +1 -0
- package/dist/hooks/use-entity-use-cases/index.js +3 -0
- package/dist/hooks/use-entity-use-cases/index.js.map +1 -0
- package/dist/hooks/use-entity-use-cases/use-entity-use-cases.d.ts +25 -0
- package/dist/hooks/use-entity-use-cases/use-entity-use-cases.d.ts.map +1 -0
- package/dist/hooks/use-entity-use-cases/use-entity-use-cases.js +47 -0
- package/dist/hooks/use-entity-use-cases/use-entity-use-cases.js.map +1 -0
- package/dist/hooks/use-wizard/index.d.ts +3 -0
- package/dist/hooks/use-wizard/index.d.ts.map +1 -0
- package/dist/hooks/use-wizard/index.js +4 -0
- package/dist/hooks/use-wizard/index.js.map +1 -0
- package/dist/hooks/use-wizard/use-wizard.d.ts +19 -0
- package/dist/hooks/use-wizard/use-wizard.d.ts.map +1 -0
- package/dist/hooks/use-wizard/use-wizard.js +136 -0
- package/dist/hooks/use-wizard/use-wizard.js.map +1 -0
- package/dist/hooks/use-wizard/use-wizard.types.d.ts +87 -0
- package/dist/hooks/use-wizard/use-wizard.types.d.ts.map +1 -0
- package/dist/hooks/use-wizard/use-wizard.types.js +3 -0
- package/dist/hooks/use-wizard/use-wizard.types.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/query/combine-filtering.d.ts +11 -0
- package/dist/query/combine-filtering.d.ts.map +1 -0
- package/dist/query/combine-filtering.js +18 -0
- package/dist/query/combine-filtering.js.map +1 -0
- package/dist/query/entity-query-key.d.ts +28 -0
- package/dist/query/entity-query-key.d.ts.map +1 -0
- package/dist/query/entity-query-key.js +28 -0
- package/dist/query/entity-query-key.js.map +1 -0
- package/dist/query/query-provider.d.ts +21 -0
- package/dist/query/query-provider.d.ts.map +1 -0
- package/dist/query/query-provider.js +35 -0
- package/dist/query/query-provider.js.map +1 -0
- package/dist/reactive/event-source-channel.d.ts +22 -0
- package/dist/reactive/event-source-channel.d.ts.map +1 -0
- package/dist/reactive/event-source-channel.js +93 -0
- package/dist/reactive/event-source-channel.js.map +1 -0
- package/dist/reactive/reactive-channel.d.ts +75 -0
- package/dist/reactive/reactive-channel.d.ts.map +1 -0
- package/dist/reactive/reactive-channel.js +55 -0
- package/dist/reactive/reactive-channel.js.map +1 -0
- package/dist/reactive/use-reactive-invalidation.d.ts +10 -0
- package/dist/reactive/use-reactive-invalidation.d.ts.map +1 -0
- package/dist/reactive/use-reactive-invalidation.js +25 -0
- package/dist/reactive/use-reactive-invalidation.js.map +1 -0
- package/dist/reactive/use-transaction-settlement.d.ts +41 -0
- package/dist/reactive/use-transaction-settlement.d.ts.map +1 -0
- package/dist/reactive/use-transaction-settlement.js +123 -0
- package/dist/reactive/use-transaction-settlement.js.map +1 -0
- package/package.json +73 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Context } from 'effect';
|
|
2
|
+
export class ReactiveChannelTag extends Context.Tag('ReactiveChannel')() {
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* A channel that never emits — the default until a transport is provided.
|
|
6
|
+
*
|
|
7
|
+
* It never connects either, so `onConnect` registers nothing: a consumer of this
|
|
8
|
+
* channel reconciles never, which is the honest behaviour for a transport that
|
|
9
|
+
* is not there.
|
|
10
|
+
*/ export const NoopReactiveChannel = {
|
|
11
|
+
subscribe: ()=>()=>undefined,
|
|
12
|
+
onConnect: ()=>()=>undefined
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* A synchronous in-memory channel: `emit` fans out to every live subscriber.
|
|
16
|
+
*
|
|
17
|
+
* It backs specs. It is no longer what the workspace runs on — that is
|
|
18
|
+
* `makeEventSourceReactiveChannel`, and until ADR 0036 landed a transport this
|
|
19
|
+
* one was mounted in its place, emitting nothing, so a reader of the workspace
|
|
20
|
+
* could not tell "the transport is missing" from "nothing changed".
|
|
21
|
+
*/ export function makeInMemoryReactiveChannel() {
|
|
22
|
+
const listeners = new Set();
|
|
23
|
+
const connectListeners = new Set();
|
|
24
|
+
let connected = false;
|
|
25
|
+
return {
|
|
26
|
+
subscribe (listener) {
|
|
27
|
+
listeners.add(listener);
|
|
28
|
+
return ()=>{
|
|
29
|
+
listeners.delete(listener);
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
onConnect (listener) {
|
|
33
|
+
connectListeners.add(listener);
|
|
34
|
+
// Mirrors the real transport: a listener arriving after the connection is
|
|
35
|
+
// already up must not wait for the next drop to hear about it.
|
|
36
|
+
if (connected) listener();
|
|
37
|
+
return ()=>{
|
|
38
|
+
connectListeners.delete(listener);
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
connect () {
|
|
42
|
+
connected = true;
|
|
43
|
+
for (const listener of connectListeners){
|
|
44
|
+
listener();
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
emit (event) {
|
|
48
|
+
for (const listener of listeners){
|
|
49
|
+
listener(event);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
//# sourceMappingURL=reactive-channel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/reactive/reactive-channel.ts"],"sourcesContent":["import type { DomainEvent, EntityChangeEvent } from '@entifix/core';\nimport { Context } from 'effect';\n\nexport type { EntityChangeEvent };\n\n/**\n * A listener on the reactive stream.\n *\n * It receives the whole message, not just the payload: the transaction id, the\n * emission time and the sequence a consumer needs are `meta.event.correlationId`,\n * `.at` and `.id`, and duplicating them onto {@link EntityChangeEvent} is how\n * two sources of one fact drift (ADR 0036). `#137` settles an optimistic write\n * on the correlation id; `useReactiveInvalidation` reads only `data.entity`.\n */\nexport type EntityChangeListener = (\n event: DomainEvent<EntityChangeEvent>,\n) => void;\n\n/**\n * The reactive-updates port. Framework-free on purpose (like the OTel tooling):\n * the transport is swappable, and the React side wires `subscribe` to the query\n * client. `subscribe` returns its own unsubscribe.\n */\nexport interface ReactiveChannel {\n subscribe(listener: EntityChangeListener): () => void;\n /**\n * Runs whenever the stream is (re)connected — and **immediately if it is\n * already connected when the listener registers**.\n *\n * The stream does not replay, by design (ADR 0036), so a consumer that missed\n * an outcome while disconnected has to go and ask. This is the signal that\n * tells it when to: `#137` re-queries every pending transaction id here.\n *\n * ⚠️ **The replay is the mechanism, not a convenience.** The channel is\n * refcounted and opened by its *first* subscriber, and\n * `useReactiveInvalidation` typically is that subscriber. A consumer mounting\n * after it — which depends on nothing more than component-tree ordering —\n * would otherwise register for an open that had already happened, and\n * reconcile nothing until the next real network drop. On a page load with no\n * disconnect, a pending set restored from the previous session would never be\n * reconciled at all, which is the case this exists for.\n *\n * Required rather than optional: an optional member on a port is how one\n * implementation quietly stops honouring it while every consumer compiles.\n */\n onConnect(listener: () => void): () => void;\n}\n\nexport class ReactiveChannelTag extends Context.Tag('ReactiveChannel')<\n ReactiveChannelTag,\n ReactiveChannel\n>() {}\n\n/**\n * A channel that never emits — the default until a transport is provided.\n *\n * It never connects either, so `onConnect` registers nothing: a consumer of this\n * channel reconciles never, which is the honest behaviour for a transport that\n * is not there.\n */\nexport const NoopReactiveChannel: ReactiveChannel = {\n subscribe: () => () => undefined,\n onConnect: () => () => undefined,\n};\n\nexport interface InMemoryReactiveChannel extends ReactiveChannel {\n /** Push an event to every current subscriber (drives tests and the mock). */\n emit(event: DomainEvent<EntityChangeEvent>): void;\n /**\n * Announce a (re)connection, so a spec can drive the reconciliation path.\n *\n * Also flips this channel to \"connected\", which is what makes a listener\n * registered afterwards fire immediately — the behaviour the real transport\n * has and the one most likely to be got wrong.\n */\n connect(): void;\n}\n\n/**\n * A synchronous in-memory channel: `emit` fans out to every live subscriber.\n *\n * It backs specs. It is no longer what the workspace runs on — that is\n * `makeEventSourceReactiveChannel`, and until ADR 0036 landed a transport this\n * one was mounted in its place, emitting nothing, so a reader of the workspace\n * could not tell \"the transport is missing\" from \"nothing changed\".\n */\nexport function makeInMemoryReactiveChannel(): InMemoryReactiveChannel {\n const listeners = new Set<EntityChangeListener>();\n const connectListeners = new Set<() => void>();\n let connected = false;\n return {\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n onConnect(listener) {\n connectListeners.add(listener);\n // Mirrors the real transport: a listener arriving after the connection is\n // already up must not wait for the next drop to hear about it.\n if (connected) listener();\n return () => {\n connectListeners.delete(listener);\n };\n },\n connect() {\n connected = true;\n for (const listener of connectListeners) {\n listener();\n }\n },\n emit(event) {\n for (const listener of listeners) {\n listener(event);\n }\n },\n };\n}\n"],"names":["Context","ReactiveChannelTag","Tag","NoopReactiveChannel","subscribe","undefined","onConnect","makeInMemoryReactiveChannel","listeners","Set","connectListeners","connected","listener","add","delete","connect","emit","event"],"mappings":"AACA,SAASA,OAAO,QAAQ,SAAS;AA+CjC,OAAO,MAAMC,2BAA2BD,QAAQE,GAAG,CAAC;AAG/C;AAEL;;;;;;CAMC,GACD,OAAO,MAAMC,sBAAuC;IAClDC,WAAW,IAAM,IAAMC;IACvBC,WAAW,IAAM,IAAMD;AACzB,EAAE;AAeF;;;;;;;CAOC,GACD,OAAO,SAASE;IACd,MAAMC,YAAY,IAAIC;IACtB,MAAMC,mBAAmB,IAAID;IAC7B,IAAIE,YAAY;IAChB,OAAO;QACLP,WAAUQ,QAAQ;YAChBJ,UAAUK,GAAG,CAACD;YACd,OAAO;gBACLJ,UAAUM,MAAM,CAACF;YACnB;QACF;QACAN,WAAUM,QAAQ;YAChBF,iBAAiBG,GAAG,CAACD;YACrB,0EAA0E;YAC1E,+DAA+D;YAC/D,IAAID,WAAWC;YACf,OAAO;gBACLF,iBAAiBI,MAAM,CAACF;YAC1B;QACF;QACAG;YACEJ,YAAY;YACZ,KAAK,MAAMC,YAAYF,iBAAkB;gBACvCE;YACF;QACF;QACAI,MAAKC,KAAK;YACR,KAAK,MAAML,YAAYJ,UAAW;gBAChCI,SAASK;YACX;QACF;IACF;AACF"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ReactiveChannel } from './reactive-channel.js';
|
|
2
|
+
/**
|
|
3
|
+
* Subscribe the query cache to a {@link ReactiveChannel}: when the server
|
|
4
|
+
* reports an entity changed, invalidate every cached query for that entity so
|
|
5
|
+
* the visible lists refetch. The key prefix `['entity', <name>]` matches
|
|
6
|
+
* {@link entityQueryKey}, so one event refreshes all pages/filters of the entity
|
|
7
|
+
* without touching unrelated caches. Optimistic writes settle the same way.
|
|
8
|
+
*/
|
|
9
|
+
export declare function useReactiveInvalidation(channel: ReactiveChannel): void;
|
|
10
|
+
//# sourceMappingURL=use-reactive-invalidation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-reactive-invalidation.d.ts","sourceRoot":"","sources":["../../src/reactive/use-reactive-invalidation.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAYtE"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
/**
|
|
5
|
+
* Subscribe the query cache to a {@link ReactiveChannel}: when the server
|
|
6
|
+
* reports an entity changed, invalidate every cached query for that entity so
|
|
7
|
+
* the visible lists refetch. The key prefix `['entity', <name>]` matches
|
|
8
|
+
* {@link entityQueryKey}, so one event refreshes all pages/filters of the entity
|
|
9
|
+
* without touching unrelated caches. Optimistic writes settle the same way.
|
|
10
|
+
*/ export function useReactiveInvalidation(channel) {
|
|
11
|
+
const queryClient = useQueryClient();
|
|
12
|
+
useEffect(()=>channel.subscribe((event)=>{
|
|
13
|
+
void queryClient.invalidateQueries({
|
|
14
|
+
queryKey: [
|
|
15
|
+
'entity',
|
|
16
|
+
event.data.entity
|
|
17
|
+
]
|
|
18
|
+
});
|
|
19
|
+
}), [
|
|
20
|
+
channel,
|
|
21
|
+
queryClient
|
|
22
|
+
]);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
//# sourceMappingURL=use-reactive-invalidation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/reactive/use-reactive-invalidation.tsx"],"sourcesContent":["'use client';\n\nimport { useQueryClient } from '@tanstack/react-query';\nimport { useEffect } from 'react';\n\nimport type { ReactiveChannel } from './reactive-channel.js';\n\n/**\n * Subscribe the query cache to a {@link ReactiveChannel}: when the server\n * reports an entity changed, invalidate every cached query for that entity so\n * the visible lists refetch. The key prefix `['entity', <name>]` matches\n * {@link entityQueryKey}, so one event refreshes all pages/filters of the entity\n * without touching unrelated caches. Optimistic writes settle the same way.\n */\nexport function useReactiveInvalidation(channel: ReactiveChannel): void {\n const queryClient = useQueryClient();\n\n useEffect(\n () =>\n channel.subscribe(event => {\n void queryClient.invalidateQueries({\n queryKey: ['entity', event.data.entity],\n });\n }),\n [channel, queryClient],\n );\n}\n"],"names":["useQueryClient","useEffect","useReactiveInvalidation","channel","queryClient","subscribe","event","invalidateQueries","queryKey","data","entity"],"mappings":"AAAA;AAEA,SAASA,cAAc,QAAQ,wBAAwB;AACvD,SAASC,SAAS,QAAQ,QAAQ;AAIlC;;;;;;CAMC,GACD,OAAO,SAASC,wBAAwBC,OAAwB;IAC9D,MAAMC,cAAcJ;IAEpBC,UACE,IACEE,QAAQE,SAAS,CAACC,CAAAA;YAChB,KAAKF,YAAYG,iBAAiB,CAAC;gBACjCC,UAAU;oBAAC;oBAAUF,MAAMG,IAAI,CAACC,MAAM;iBAAC;YACzC;QACF,IACF;QAACP;QAASC;KAAY;AAE1B"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { PendingTransactionStore, TransactionRecord } from '@entifix/transactions';
|
|
2
|
+
import type { ReactiveChannel } from './reactive-channel.js';
|
|
3
|
+
export interface UseTransactionSettlementOptions {
|
|
4
|
+
/** The stream outcomes arrive on, and whose reconnects trigger a re-query. */
|
|
5
|
+
channel: ReactiveChannel;
|
|
6
|
+
/**
|
|
7
|
+
* The writes being watched.
|
|
8
|
+
*
|
|
9
|
+
* Passed in rather than imported: the store is a shell concern
|
|
10
|
+
* (`layer:shell`) and this hook is `layer:entifix`, which may not depend
|
|
11
|
+
* upward. Same seam, and the same reason, as `EntityDraftStore` for autosave.
|
|
12
|
+
*/
|
|
13
|
+
pending: PendingTransactionStore;
|
|
14
|
+
/** Reads one record. `undefined` means *not tracked yet* — never *failed*. */
|
|
15
|
+
read: (transactionId: string) => Promise<TransactionRecord | undefined>;
|
|
16
|
+
pollIntervalMs?: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Settles the optimistic writes this browser started.
|
|
20
|
+
*
|
|
21
|
+
* Three things drive it, and they exist for three different failures:
|
|
22
|
+
*
|
|
23
|
+
* - **The stream**, the fast path: `transaction.completed` and
|
|
24
|
+
* `transaction.failed` carry the correlation id, so an outcome lands in the
|
|
25
|
+
* moment it happens.
|
|
26
|
+
* - **`onConnect`**, because the stream does not replay (ADR 0036). It fires on
|
|
27
|
+
* every open *including for a listener registered while already connected*, so
|
|
28
|
+
* a page load holding a restored pending set and an in-session reconnect take
|
|
29
|
+
* one path rather than two.
|
|
30
|
+
* - **An interval**, because `STALE` is written by the recovery sweep and emits
|
|
31
|
+
* no event at all — it is reachable only by asking. It runs only while
|
|
32
|
+
* something is pending, so an idle workspace polls nothing.
|
|
33
|
+
*
|
|
34
|
+
* ⚠️ **A missing record keeps the entry pending.** `accepted` reaches the
|
|
35
|
+
* tracker over the bus, so with the broker down the entity write commits while
|
|
36
|
+
* no event is ever published and the tracker holds nothing. Rolling back there
|
|
37
|
+
* un-renders a healthy write at exactly the moment nobody can tell a UI bug from
|
|
38
|
+
* an outage. Only `FAILED` and `STALE` are terminal.
|
|
39
|
+
*/
|
|
40
|
+
export declare function useTransactionSettlement({ channel, pending, read, pollIntervalMs, }: UseTransactionSettlementOptions): void;
|
|
41
|
+
//# sourceMappingURL=use-transaction-settlement.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-transaction-settlement.d.ts","sourceRoot":"","sources":["../../src/reactive/use-transaction-settlement.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAEV,uBAAuB,EACvB,iBAAiB,EAClB,MAAM,uBAAuB,CAAC;AAK/B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAK7D,MAAM,WAAW,+BAA+B;IAC9C,8EAA8E;IAC9E,OAAO,EAAE,eAAe,CAAC;IACzB;;;;;;OAMG;IACH,OAAO,EAAE,uBAAuB,CAAC;IACjC,8EAA8E;IAC9E,IAAI,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAAC;IACxE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,wBAAwB,CAAC,EACvC,OAAO,EACP,OAAO,EACP,IAAI,EACJ,cAAgC,GACjC,EAAE,+BAA+B,GAAG,IAAI,CA2GxC"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
3
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
4
|
+
import { entityQueryScopeFor } from '../query/entity-query-key.js';
|
|
5
|
+
/** How often to re-ask while anything is pending. */ const DEFAULT_POLL_MS = 30000;
|
|
6
|
+
/**
|
|
7
|
+
* Settles the optimistic writes this browser started.
|
|
8
|
+
*
|
|
9
|
+
* Three things drive it, and they exist for three different failures:
|
|
10
|
+
*
|
|
11
|
+
* - **The stream**, the fast path: `transaction.completed` and
|
|
12
|
+
* `transaction.failed` carry the correlation id, so an outcome lands in the
|
|
13
|
+
* moment it happens.
|
|
14
|
+
* - **`onConnect`**, because the stream does not replay (ADR 0036). It fires on
|
|
15
|
+
* every open *including for a listener registered while already connected*, so
|
|
16
|
+
* a page load holding a restored pending set and an in-session reconnect take
|
|
17
|
+
* one path rather than two.
|
|
18
|
+
* - **An interval**, because `STALE` is written by the recovery sweep and emits
|
|
19
|
+
* no event at all — it is reachable only by asking. It runs only while
|
|
20
|
+
* something is pending, so an idle workspace polls nothing.
|
|
21
|
+
*
|
|
22
|
+
* ⚠️ **A missing record keeps the entry pending.** `accepted` reaches the
|
|
23
|
+
* tracker over the bus, so with the broker down the entity write commits while
|
|
24
|
+
* no event is ever published and the tracker holds nothing. Rolling back there
|
|
25
|
+
* un-renders a healthy write at exactly the moment nobody can tell a UI bug from
|
|
26
|
+
* an outage. Only `FAILED` and `STALE` are terminal.
|
|
27
|
+
*/ export function useTransactionSettlement({ channel, pending, read, pollIntervalMs = DEFAULT_POLL_MS }) {
|
|
28
|
+
const queryClient = useQueryClient();
|
|
29
|
+
// The store is rebuilt whenever an entry changes and `read` is typically
|
|
30
|
+
// inline, so both are held in refs: keying the subscription on their identity
|
|
31
|
+
// would tear down and re-open the stream every time a write settled.
|
|
32
|
+
const pendingRef = useRef(pending);
|
|
33
|
+
const readRef = useRef(read);
|
|
34
|
+
// Refs may not be written during render. This runs before the effects below,
|
|
35
|
+
// since effects fire in declaration order, so they always see the latest.
|
|
36
|
+
useEffect(()=>{
|
|
37
|
+
pendingRef.current = pending;
|
|
38
|
+
readRef.current = read;
|
|
39
|
+
});
|
|
40
|
+
const invalidate = useCallback((entity)=>void queryClient.invalidateQueries({
|
|
41
|
+
queryKey: entityQueryScopeFor(entity)
|
|
42
|
+
}), [
|
|
43
|
+
queryClient
|
|
44
|
+
]);
|
|
45
|
+
/**
|
|
46
|
+
* Applies whatever the tracker currently says about one entry.
|
|
47
|
+
*
|
|
48
|
+
* The single place the terminal-state rule lives — the stream path, the
|
|
49
|
+
* reconnect path and the interval all come through here, so there is one
|
|
50
|
+
* answer to "what does this state mean" rather than three that can drift.
|
|
51
|
+
*/ const reconcile = useCallback(async (entry)=>{
|
|
52
|
+
const record = await readRef.current(entry.transactionId).catch(// The tracker being unreachable says nothing about the write. Keeping
|
|
53
|
+
// the entry is what makes a service restart converge instead of dropping
|
|
54
|
+
// every optimistic row on the floor.
|
|
55
|
+
()=>undefined);
|
|
56
|
+
// ⚠️ Not tracked yet, or still running. Either way it is not over, and
|
|
57
|
+
// this is the branch that must not be turned into a rollback.
|
|
58
|
+
if (record === undefined || record.state === 'PENDING') return;
|
|
59
|
+
if (record.state === 'COMPLETED') {
|
|
60
|
+
pendingRef.current.settle(entry.transactionId);
|
|
61
|
+
} else {
|
|
62
|
+
// `record.error` is free text from whatever threw — diagnostic detail
|
|
63
|
+
// beside a translated headline, never the message itself.
|
|
64
|
+
pendingRef.current.fail(entry.transactionId, record.error);
|
|
65
|
+
}
|
|
66
|
+
invalidate(entry.entity);
|
|
67
|
+
}, [
|
|
68
|
+
invalidate
|
|
69
|
+
]);
|
|
70
|
+
const reconcileAll = useCallback(()=>{
|
|
71
|
+
for (const entry of pendingRef.current.entries){
|
|
72
|
+
if (entry.state === 'pending') void reconcile(entry);
|
|
73
|
+
}
|
|
74
|
+
}, [
|
|
75
|
+
reconcile
|
|
76
|
+
]);
|
|
77
|
+
useEffect(()=>{
|
|
78
|
+
const stopListening = channel.subscribe((event)=>{
|
|
79
|
+
const transactionId = event.correlationId;
|
|
80
|
+
if (transactionId === undefined) return;
|
|
81
|
+
const entry = pendingRef.current.entries.find((candidate)=>candidate.transactionId === transactionId);
|
|
82
|
+
// An id this browser never started belongs to another tab or another
|
|
83
|
+
// session — the stream is organization-scoped, not connection-scoped.
|
|
84
|
+
if (entry === undefined) return;
|
|
85
|
+
if (event.name === 'transaction.completed') {
|
|
86
|
+
pendingRef.current.settle(transactionId);
|
|
87
|
+
invalidate(event.data.entity);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (event.name === 'transaction.failed') {
|
|
91
|
+
// The frame carries no reason: `EntityChangeEvent` collapses four states
|
|
92
|
+
// into created/deleted and holds no `error`. So the record is re-read
|
|
93
|
+
// rather than the payload widened (ADR 0036's hint-versus-record split).
|
|
94
|
+
void reconcile(entry);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
const stopWatchingConnections = channel.onConnect(reconcileAll);
|
|
98
|
+
return ()=>{
|
|
99
|
+
stopListening();
|
|
100
|
+
stopWatchingConnections();
|
|
101
|
+
};
|
|
102
|
+
}, [
|
|
103
|
+
channel,
|
|
104
|
+
invalidate,
|
|
105
|
+
reconcile,
|
|
106
|
+
reconcileAll
|
|
107
|
+
]);
|
|
108
|
+
// Keyed on whether anything is watched at all, not on the entries: that flips
|
|
109
|
+
// once per transition rather than on every settle, so the timer is not torn
|
|
110
|
+
// down and rebuilt each time a write lands.
|
|
111
|
+
const watching = pending.entries.length > 0;
|
|
112
|
+
useEffect(()=>{
|
|
113
|
+
if (!watching) return;
|
|
114
|
+
const timer = setInterval(reconcileAll, pollIntervalMs);
|
|
115
|
+
return ()=>clearInterval(timer);
|
|
116
|
+
}, [
|
|
117
|
+
watching,
|
|
118
|
+
pollIntervalMs,
|
|
119
|
+
reconcileAll
|
|
120
|
+
]);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
//# sourceMappingURL=use-transaction-settlement.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/reactive/use-transaction-settlement.tsx"],"sourcesContent":["'use client';\n\nimport type {\n PendingEntry,\n PendingTransactionStore,\n TransactionRecord,\n} from '@entifix/transactions';\nimport { useQueryClient } from '@tanstack/react-query';\nimport { useCallback, useEffect, useRef } from 'react';\n\nimport { entityQueryScopeFor } from '../query/entity-query-key.js';\nimport type { ReactiveChannel } from './reactive-channel.js';\n\n/** How often to re-ask while anything is pending. */\nconst DEFAULT_POLL_MS = 30_000;\n\nexport interface UseTransactionSettlementOptions {\n /** The stream outcomes arrive on, and whose reconnects trigger a re-query. */\n channel: ReactiveChannel;\n /**\n * The writes being watched.\n *\n * Passed in rather than imported: the store is a shell concern\n * (`layer:shell`) and this hook is `layer:entifix`, which may not depend\n * upward. Same seam, and the same reason, as `EntityDraftStore` for autosave.\n */\n pending: PendingTransactionStore;\n /** Reads one record. `undefined` means *not tracked yet* — never *failed*. */\n read: (transactionId: string) => Promise<TransactionRecord | undefined>;\n pollIntervalMs?: number;\n}\n\n/**\n * Settles the optimistic writes this browser started.\n *\n * Three things drive it, and they exist for three different failures:\n *\n * - **The stream**, the fast path: `transaction.completed` and\n * `transaction.failed` carry the correlation id, so an outcome lands in the\n * moment it happens.\n * - **`onConnect`**, because the stream does not replay (ADR 0036). It fires on\n * every open *including for a listener registered while already connected*, so\n * a page load holding a restored pending set and an in-session reconnect take\n * one path rather than two.\n * - **An interval**, because `STALE` is written by the recovery sweep and emits\n * no event at all — it is reachable only by asking. It runs only while\n * something is pending, so an idle workspace polls nothing.\n *\n * ⚠️ **A missing record keeps the entry pending.** `accepted` reaches the\n * tracker over the bus, so with the broker down the entity write commits while\n * no event is ever published and the tracker holds nothing. Rolling back there\n * un-renders a healthy write at exactly the moment nobody can tell a UI bug from\n * an outage. Only `FAILED` and `STALE` are terminal.\n */\nexport function useTransactionSettlement({\n channel,\n pending,\n read,\n pollIntervalMs = DEFAULT_POLL_MS,\n}: UseTransactionSettlementOptions): void {\n const queryClient = useQueryClient();\n\n // The store is rebuilt whenever an entry changes and `read` is typically\n // inline, so both are held in refs: keying the subscription on their identity\n // would tear down and re-open the stream every time a write settled.\n const pendingRef = useRef(pending);\n const readRef = useRef(read);\n\n // Refs may not be written during render. This runs before the effects below,\n // since effects fire in declaration order, so they always see the latest.\n useEffect(() => {\n pendingRef.current = pending;\n readRef.current = read;\n });\n\n const invalidate = useCallback(\n (entity: string) =>\n void queryClient.invalidateQueries({\n queryKey: entityQueryScopeFor(entity),\n }),\n [queryClient],\n );\n\n /**\n * Applies whatever the tracker currently says about one entry.\n *\n * The single place the terminal-state rule lives — the stream path, the\n * reconnect path and the interval all come through here, so there is one\n * answer to \"what does this state mean\" rather than three that can drift.\n */\n const reconcile = useCallback(\n async (entry: PendingEntry): Promise<void> => {\n const record = await readRef.current(entry.transactionId).catch(\n // The tracker being unreachable says nothing about the write. Keeping\n // the entry is what makes a service restart converge instead of dropping\n // every optimistic row on the floor.\n () => undefined,\n );\n\n // ⚠️ Not tracked yet, or still running. Either way it is not over, and\n // this is the branch that must not be turned into a rollback.\n if (record === undefined || record.state === 'PENDING') return;\n\n if (record.state === 'COMPLETED') {\n pendingRef.current.settle(entry.transactionId);\n } else {\n // `record.error` is free text from whatever threw — diagnostic detail\n // beside a translated headline, never the message itself.\n pendingRef.current.fail(entry.transactionId, record.error);\n }\n invalidate(entry.entity);\n },\n [invalidate],\n );\n\n const reconcileAll = useCallback(() => {\n for (const entry of pendingRef.current.entries) {\n if (entry.state === 'pending') void reconcile(entry);\n }\n }, [reconcile]);\n\n useEffect(() => {\n const stopListening = channel.subscribe(event => {\n const transactionId = event.correlationId;\n if (transactionId === undefined) return;\n\n const entry = pendingRef.current.entries.find(\n candidate => candidate.transactionId === transactionId,\n );\n // An id this browser never started belongs to another tab or another\n // session — the stream is organization-scoped, not connection-scoped.\n if (entry === undefined) return;\n\n if (event.name === 'transaction.completed') {\n pendingRef.current.settle(transactionId);\n invalidate(event.data.entity);\n return;\n }\n\n if (event.name === 'transaction.failed') {\n // The frame carries no reason: `EntityChangeEvent` collapses four states\n // into created/deleted and holds no `error`. So the record is re-read\n // rather than the payload widened (ADR 0036's hint-versus-record split).\n void reconcile(entry);\n }\n });\n\n const stopWatchingConnections = channel.onConnect(reconcileAll);\n\n return () => {\n stopListening();\n stopWatchingConnections();\n };\n }, [channel, invalidate, reconcile, reconcileAll]);\n\n // Keyed on whether anything is watched at all, not on the entries: that flips\n // once per transition rather than on every settle, so the timer is not torn\n // down and rebuilt each time a write lands.\n const watching = pending.entries.length > 0;\n\n useEffect(() => {\n if (!watching) return;\n\n const timer = setInterval(reconcileAll, pollIntervalMs);\n return () => clearInterval(timer);\n }, [watching, pollIntervalMs, reconcileAll]);\n}\n"],"names":["useQueryClient","useCallback","useEffect","useRef","entityQueryScopeFor","DEFAULT_POLL_MS","useTransactionSettlement","channel","pending","read","pollIntervalMs","queryClient","pendingRef","readRef","current","invalidate","entity","invalidateQueries","queryKey","reconcile","entry","record","transactionId","catch","undefined","state","settle","fail","error","reconcileAll","entries","stopListening","subscribe","event","correlationId","find","candidate","name","data","stopWatchingConnections","onConnect","watching","length","timer","setInterval","clearInterval"],"mappings":"AAAA;AAOA,SAASA,cAAc,QAAQ,wBAAwB;AACvD,SAASC,WAAW,EAAEC,SAAS,EAAEC,MAAM,QAAQ,QAAQ;AAEvD,SAASC,mBAAmB,QAAQ,+BAA+B;AAGnE,mDAAmD,GACnD,MAAMC,kBAAkB;AAkBxB;;;;;;;;;;;;;;;;;;;;;CAqBC,GACD,OAAO,SAASC,yBAAyB,EACvCC,OAAO,EACPC,OAAO,EACPC,IAAI,EACJC,iBAAiBL,eAAe,EACA;IAChC,MAAMM,cAAcX;IAEpB,yEAAyE;IACzE,8EAA8E;IAC9E,qEAAqE;IACrE,MAAMY,aAAaT,OAAOK;IAC1B,MAAMK,UAAUV,OAAOM;IAEvB,6EAA6E;IAC7E,0EAA0E;IAC1EP,UAAU;QACRU,WAAWE,OAAO,GAAGN;QACrBK,QAAQC,OAAO,GAAGL;IACpB;IAEA,MAAMM,aAAad,YACjB,CAACe,SACC,KAAKL,YAAYM,iBAAiB,CAAC;YACjCC,UAAUd,oBAAoBY;QAChC,IACF;QAACL;KAAY;IAGf;;;;;;GAMC,GACD,MAAMQ,YAAYlB,YAChB,OAAOmB;QACL,MAAMC,SAAS,MAAMR,QAAQC,OAAO,CAACM,MAAME,aAAa,EAAEC,KAAK,CAC7D,sEAAsE;QACtE,yEAAyE;QACzE,qCAAqC;QACrC,IAAMC;QAGR,uEAAuE;QACvE,8DAA8D;QAC9D,IAAIH,WAAWG,aAAaH,OAAOI,KAAK,KAAK,WAAW;QAExD,IAAIJ,OAAOI,KAAK,KAAK,aAAa;YAChCb,WAAWE,OAAO,CAACY,MAAM,CAACN,MAAME,aAAa;QAC/C,OAAO;YACL,sEAAsE;YACtE,0DAA0D;YAC1DV,WAAWE,OAAO,CAACa,IAAI,CAACP,MAAME,aAAa,EAAED,OAAOO,KAAK;QAC3D;QACAb,WAAWK,MAAMJ,MAAM;IACzB,GACA;QAACD;KAAW;IAGd,MAAMc,eAAe5B,YAAY;QAC/B,KAAK,MAAMmB,SAASR,WAAWE,OAAO,CAACgB,OAAO,CAAE;YAC9C,IAAIV,MAAMK,KAAK,KAAK,WAAW,KAAKN,UAAUC;QAChD;IACF,GAAG;QAACD;KAAU;IAEdjB,UAAU;QACR,MAAM6B,gBAAgBxB,QAAQyB,SAAS,CAACC,CAAAA;YACtC,MAAMX,gBAAgBW,MAAMC,aAAa;YACzC,IAAIZ,kBAAkBE,WAAW;YAEjC,MAAMJ,QAAQR,WAAWE,OAAO,CAACgB,OAAO,CAACK,IAAI,CAC3CC,CAAAA,YAAaA,UAAUd,aAAa,KAAKA;YAE3C,qEAAqE;YACrE,sEAAsE;YACtE,IAAIF,UAAUI,WAAW;YAEzB,IAAIS,MAAMI,IAAI,KAAK,yBAAyB;gBAC1CzB,WAAWE,OAAO,CAACY,MAAM,CAACJ;gBAC1BP,WAAWkB,MAAMK,IAAI,CAACtB,MAAM;gBAC5B;YACF;YAEA,IAAIiB,MAAMI,IAAI,KAAK,sBAAsB;gBACvC,yEAAyE;gBACzE,sEAAsE;gBACtE,yEAAyE;gBACzE,KAAKlB,UAAUC;YACjB;QACF;QAEA,MAAMmB,0BAA0BhC,QAAQiC,SAAS,CAACX;QAElD,OAAO;YACLE;YACAQ;QACF;IACF,GAAG;QAAChC;QAASQ;QAAYI;QAAWU;KAAa;IAEjD,8EAA8E;IAC9E,4EAA4E;IAC5E,4CAA4C;IAC5C,MAAMY,WAAWjC,QAAQsB,OAAO,CAACY,MAAM,GAAG;IAE1CxC,UAAU;QACR,IAAI,CAACuC,UAAU;QAEf,MAAME,QAAQC,YAAYf,cAAcnB;QACxC,OAAO,IAAMmC,cAAcF;IAC7B,GAAG;QAACF;QAAU/B;QAAgBmB;KAAa;AAC7C"}
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@entifix/react-integration",
|
|
3
|
+
"nx": {
|
|
4
|
+
"tags": [
|
|
5
|
+
"layer:entifix",
|
|
6
|
+
"scope:shared",
|
|
7
|
+
"entifix:react",
|
|
8
|
+
"tier:3"
|
|
9
|
+
],
|
|
10
|
+
"sourceRoot": "packages/react/integration/src",
|
|
11
|
+
"targets": {
|
|
12
|
+
"build": {
|
|
13
|
+
"executor": "@nx/js:swc",
|
|
14
|
+
"outputs": [
|
|
15
|
+
"{options.outputPath}"
|
|
16
|
+
],
|
|
17
|
+
"options": {
|
|
18
|
+
"outputPath": "packages/react/integration/dist",
|
|
19
|
+
"main": "packages/react/integration/src/index.ts",
|
|
20
|
+
"tsConfig": "packages/react/integration/tsconfig.lib.json",
|
|
21
|
+
"skipTypeCheck": true,
|
|
22
|
+
"stripLeadingPaths": true
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"version": "0.1.0-0",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/r10c-technologies/entifix.git",
|
|
32
|
+
"directory": "packages/react/integration"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"type": "module",
|
|
38
|
+
"main": "./dist/index.js",
|
|
39
|
+
"module": "./dist/index.js",
|
|
40
|
+
"types": "./dist/index.d.ts",
|
|
41
|
+
"exports": {
|
|
42
|
+
"./package.json": "./package.json",
|
|
43
|
+
".": {
|
|
44
|
+
"@entifix/source": "./src/index.ts",
|
|
45
|
+
"types": "./dist/index.d.ts",
|
|
46
|
+
"import": "./dist/index.js",
|
|
47
|
+
"default": "./dist/index.js"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"files": [
|
|
51
|
+
"dist",
|
|
52
|
+
"!**/*.tsbuildinfo",
|
|
53
|
+
"LICENSE"
|
|
54
|
+
],
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@entifix/transactions": "0.1.0-0",
|
|
57
|
+
"@entifix/business": "0.1.0-0",
|
|
58
|
+
"@entifix/core": "0.1.0-0"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@tanstack/react-form": "1.33.5",
|
|
62
|
+
"@tanstack/react-query": "^5.101.4",
|
|
63
|
+
"effect": "3.22.1",
|
|
64
|
+
"react": "19.2.8",
|
|
65
|
+
"@entifix/testing-unit": "0.1.0-0"
|
|
66
|
+
},
|
|
67
|
+
"peerDependencies": {
|
|
68
|
+
"@tanstack/react-form": ">=1.33 <2",
|
|
69
|
+
"@tanstack/react-query": ">=5.101 <6",
|
|
70
|
+
"effect": ">=3.22 <4",
|
|
71
|
+
"react": ">=19 <20"
|
|
72
|
+
}
|
|
73
|
+
}
|