@entifix/react-integration 0.4.1 → 0.5.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/README.md CHANGED
@@ -12,10 +12,11 @@ pnpm add @entifix/react-integration @tanstack/react-form @tanstack/react-query e
12
12
 
13
13
  ## Entry points
14
14
 
15
- | Import | What it holds |
16
- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
17
- | `@entifix/react-integration` | `EntifixQueryProvider`, `useEntityRecord`, `useEntityForm`, `useEntityMutation`, `useWizard`, `useReactiveInvalidation`, `useTransactionSettlement` |
18
- | `@entifix/react-integration/url` | `defineUrlQuery`, `useUrlState`, `useEntityLoad`, `useUrlFilter`: a filter kept in the query string, answered by `loadAllUCFactory` over an `EntitySource`. No TanStack |
15
+ | Import | What it holds |
16
+ | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
17
+ | `@entifix/react-integration` | `EntifixQueryProvider`, `useEntityRecord`, `useEntityForm`, `useEntityMutation`, `useWizard`, `useReactiveInvalidation`, `useTransactionSettlement` |
18
+ | `@entifix/react-integration/url` | `defineUrlQuery`, `useUrlState`, `useEntityLoad`, `useUrlFilter`: a filter kept in the query string, answered by `loadAllUCFactory` over an `EntitySource`. No TanStack |
19
+ | `@entifix/react-integration/event-source` | `makeEventSourceReactiveChannel`, the browser transport behind `ReactiveChannelTag` — imported by the composition root that picks it, never by a component |
19
20
 
20
21
  ---
21
22
 
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The browser transport behind `ReactiveChannelTag`: one `EventSource` over a
3
+ * same-origin URL.
4
+ *
5
+ * Its own entry point because choosing the transport and using the port are
6
+ * different acts. A component that reacts to change events reads
7
+ * `ReactiveChannelTag` from `.`; only the composition root that picks SSE
8
+ * imports this (ADR 0006).
9
+ */
10
+ export * from './reactive/event-source-channel.js';
11
+ //# sourceMappingURL=event-source.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-source.d.ts","sourceRoot":"","sources":["../src/event-source.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,cAAc,oCAAoC,CAAC"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The browser transport behind `ReactiveChannelTag`: one `EventSource` over a
3
+ * same-origin URL.
4
+ *
5
+ * Its own entry point because choosing the transport and using the port are
6
+ * different acts. A component that reacts to change events reads
7
+ * `ReactiveChannelTag` from `.`; only the composition root that picks SSE
8
+ * imports this (ADR 0006).
9
+ */ export * from './reactive/event-source-channel.js';
10
+
11
+ //# sourceMappingURL=event-source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/event-source.ts"],"sourcesContent":["/**\n * The browser transport behind `ReactiveChannelTag`: one `EventSource` over a\n * same-origin URL.\n *\n * Its own entry point because choosing the transport and using the port are\n * different acts. A component that reacts to change events reads\n * `ReactiveChannelTag` from `.`; only the composition root that picks SSE\n * imports this (ADR 0006).\n */\nexport * from './reactive/event-source-channel.js';\n"],"names":[],"mappings":"AAAA;;;;;;;;CAQC,GACD,cAAc,qCAAqC"}
@@ -1,4 +1,4 @@
1
- import { type ConfigurationRepositoryTag, createEntityLinkResolver, type EntityLinkRegistration, type EntityLinkResolverTag } from '@entifix/business';
1
+ import { createEntityLinkResolver, type EntityLinkRegistration, type EntityLinkResolverTag } from '@entifix/business';
2
2
  import type { Context } from 'effect';
3
3
  export type { EntityLinkRegistration };
4
4
  export { createEntityLinkResolver };
@@ -13,5 +13,5 @@ export { createEntityLinkResolver };
13
13
  * component needs it without dragging this package's hooks along. Re-exported
14
14
  * here so existing client call sites keep one import.
15
15
  */
16
- export declare function useEntityLinkResolver(configurationStore: Context.Context<ConfigurationRepositoryTag>, registrations: ReadonlyArray<EntityLinkRegistration>): Context.Context<EntityLinkResolverTag>;
16
+ export declare function useEntityLinkResolver(registrations: ReadonlyArray<EntityLinkRegistration>): Context.Context<EntityLinkResolverTag>;
17
17
  //# sourceMappingURL=use-entity-link-resolver.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-entity-link-resolver.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-entity-link-resolver/use-entity-link-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,0BAA0B,EAC/B,wBAAwB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC3B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAGtC,YAAY,EAAE,sBAAsB,EAAE,CAAC;AACvC,OAAO,EAAE,wBAAwB,EAAE,CAAC;AAEpC;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CACnC,kBAAkB,EAAE,OAAO,CAAC,OAAO,CAAC,0BAA0B,CAAC,EAC/D,aAAa,EAAE,aAAa,CAAC,sBAAsB,CAAC,GACnD,OAAO,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAaxC"}
1
+ {"version":3,"file":"use-entity-link-resolver.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-entity-link-resolver/use-entity-link-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,wBAAwB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC3B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAGtC,YAAY,EAAE,sBAAsB,EAAE,CAAC;AACvC,OAAO,EAAE,wBAAwB,EAAE,CAAC;AAEpC;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CACnC,aAAa,EAAE,aAAa,CAAC,sBAAsB,CAAC,GACnD,OAAO,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAaxC"}
@@ -11,14 +11,13 @@ export { createEntityLinkResolver };
11
11
  * `@entifix/business` — nothing about it is React, and a server
12
12
  * component needs it without dragging this package's hooks along. Re-exported
13
13
  * here so existing client call sites keep one import.
14
- */ export function useEntityLinkResolver(configurationStore, registrations) {
14
+ */ export function useEntityLinkResolver(registrations) {
15
15
  // Registrations are a static list per page; key the memo on the registered
16
16
  // constructor names so it stays stable across renders without depending on the
17
17
  // array literal's identity.
18
18
  const registrationKey = registrations.map(([entityConstructor])=>entityConstructor.name).join('|');
19
- return useMemo(()=>createEntityLinkResolver(configurationStore, registrations), // eslint-disable-next-line react-hooks/exhaustive-deps
19
+ return useMemo(()=>createEntityLinkResolver(registrations), // eslint-disable-next-line react-hooks/exhaustive-deps
20
20
  [
21
- configurationStore,
22
21
  registrationKey
23
22
  ]);
24
23
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/hooks/use-entity-link-resolver/use-entity-link-resolver.ts"],"sourcesContent":["import {\n type ConfigurationRepositoryTag,\n createEntityLinkResolver,\n type EntityLinkRegistration,\n type EntityLinkResolverTag,\n} from '@entifix/business';\nimport type { Context } from 'effect';\nimport { useMemo } from 'react';\n\nexport type { EntityLinkRegistration };\nexport { createEntityLinkResolver };\n\n/**\n * React hook that memoizes an {@link EntityLinkResolverTag} context built from\n * the given repository adapters. This is the page-level seam for last-mile\n * wiring: a caller can register cached or state-backed adapters here without\n * touching the base adapters or the use-case.\n *\n * The assembly itself is `createEntityLinkResolver` in\n * `@entifix/business` — nothing about it is React, and a server\n * component needs it without dragging this package's hooks along. Re-exported\n * here so existing client call sites keep one import.\n */\nexport function useEntityLinkResolver(\n configurationStore: Context.Context<ConfigurationRepositoryTag>,\n registrations: ReadonlyArray<EntityLinkRegistration>,\n): Context.Context<EntityLinkResolverTag> {\n // Registrations are a static list per page; key the memo on the registered\n // constructor names so it stays stable across renders without depending on the\n // array literal's identity.\n const registrationKey = registrations\n .map(([entityConstructor]) => entityConstructor.name)\n .join('|');\n\n return useMemo(\n () => createEntityLinkResolver(configurationStore, registrations),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [configurationStore, registrationKey],\n );\n}\n"],"names":["createEntityLinkResolver","useMemo","useEntityLinkResolver","configurationStore","registrations","registrationKey","map","entityConstructor","name","join"],"mappings":"AAAA,SAEEA,wBAAwB,QAGnB,oBAAoB;AAE3B,SAASC,OAAO,QAAQ,QAAQ;AAGhC,SAASD,wBAAwB,GAAG;AAEpC;;;;;;;;;;CAUC,GACD,OAAO,SAASE,sBACdC,kBAA+D,EAC/DC,aAAoD;IAEpD,2EAA2E;IAC3E,+EAA+E;IAC/E,4BAA4B;IAC5B,MAAMC,kBAAkBD,cACrBE,GAAG,CAAC,CAAC,CAACC,kBAAkB,GAAKA,kBAAkBC,IAAI,EACnDC,IAAI,CAAC;IAER,OAAOR,QACL,IAAMD,yBAAyBG,oBAAoBC,gBACnD,uDAAuD;IACvD;QAACD;QAAoBE;KAAgB;AAEzC"}
1
+ {"version":3,"sources":["../../../src/hooks/use-entity-link-resolver/use-entity-link-resolver.ts"],"sourcesContent":["import {\n createEntityLinkResolver,\n type EntityLinkRegistration,\n type EntityLinkResolverTag,\n} from '@entifix/business';\nimport type { Context } from 'effect';\nimport { useMemo } from 'react';\n\nexport type { EntityLinkRegistration };\nexport { createEntityLinkResolver };\n\n/**\n * React hook that memoizes an {@link EntityLinkResolverTag} context built from\n * the given repository adapters. This is the page-level seam for last-mile\n * wiring: a caller can register cached or state-backed adapters here without\n * touching the base adapters or the use-case.\n *\n * The assembly itself is `createEntityLinkResolver` in\n * `@entifix/business` — nothing about it is React, and a server\n * component needs it without dragging this package's hooks along. Re-exported\n * here so existing client call sites keep one import.\n */\nexport function useEntityLinkResolver(\n registrations: ReadonlyArray<EntityLinkRegistration>,\n): Context.Context<EntityLinkResolverTag> {\n // Registrations are a static list per page; key the memo on the registered\n // constructor names so it stays stable across renders without depending on the\n // array literal's identity.\n const registrationKey = registrations\n .map(([entityConstructor]) => entityConstructor.name)\n .join('|');\n\n return useMemo(\n () => createEntityLinkResolver(registrations),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [registrationKey],\n );\n}\n"],"names":["createEntityLinkResolver","useMemo","useEntityLinkResolver","registrations","registrationKey","map","entityConstructor","name","join"],"mappings":"AAAA,SACEA,wBAAwB,QAGnB,oBAAoB;AAE3B,SAASC,OAAO,QAAQ,QAAQ;AAGhC,SAASD,wBAAwB,GAAG;AAEpC;;;;;;;;;;CAUC,GACD,OAAO,SAASE,sBACdC,aAAoD;IAEpD,2EAA2E;IAC3E,+EAA+E;IAC/E,4BAA4B;IAC5B,MAAMC,kBAAkBD,cACrBE,GAAG,CAAC,CAAC,CAACC,kBAAkB,GAAKA,kBAAkBC,IAAI,EACnDC,IAAI,CAAC;IAER,OAAOP,QACL,IAAMD,yBAAyBG,gBAC/B,uDAAuD;IACvD;QAACC;KAAgB;AAErB"}
package/dist/index.d.ts CHANGED
@@ -10,7 +10,6 @@ export * from './hooks/use-wizard/index.js';
10
10
  export * from './query/combine-filtering.js';
11
11
  export * from './query/entity-query-key.js';
12
12
  export * from './query/query-provider.js';
13
- export * from './reactive/event-source-channel.js';
14
13
  export * from './reactive/reactive-channel.js';
15
14
  export * from './reactive/use-reactive-invalidation.js';
16
15
  export * from './reactive/use-transaction-settlement.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qCAAqC,CAAC;AACpD,cAAc,mCAAmC,CAAC;AAClD,cAAc,kCAAkC,CAAC;AACjD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,yCAAyC,CAAC;AACxD,cAAc,sCAAsC,CAAC;AACrD,cAAc,oCAAoC,CAAC;AACnD,cAAc,uCAAuC,CAAC;AACtD,cAAc,6BAA6B,CAAC;AAC5C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,oCAAoC,CAAC;AACnD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,yCAAyC,CAAC;AACxD,cAAc,0CAA0C,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qCAAqC,CAAC;AACpD,cAAc,mCAAmC,CAAC;AAClD,cAAc,kCAAkC,CAAC;AACjD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,yCAAyC,CAAC;AACxD,cAAc,sCAAsC,CAAC;AACrD,cAAc,oCAAoC,CAAC;AACnD,cAAc,uCAAuC,CAAC;AACtD,cAAc,6BAA6B,CAAC;AAC5C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,yCAAyC,CAAC;AACxD,cAAc,0CAA0C,CAAC"}
package/dist/index.js CHANGED
@@ -10,7 +10,6 @@ export * from './hooks/use-wizard/index.js';
10
10
  export * from './query/combine-filtering.js';
11
11
  export * from './query/entity-query-key.js';
12
12
  export * from './query/query-provider.js';
13
- export * from './reactive/event-source-channel.js';
14
13
  export * from './reactive/reactive-channel.js';
15
14
  export * from './reactive/use-reactive-invalidation.js';
16
15
  export * from './reactive/use-transaction-settlement.js';
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './context/adapters-context/index.js';\nexport * from './hooks/use-data-loading/index.js';\nexport * from './hooks/use-entity-form/index.js';\nexport * from './hooks/use-entity-link-resolver/index.js';\nexport * from './hooks/use-entity-link-source/index.js';\nexport * from './hooks/use-entity-mutation/index.js';\nexport * from './hooks/use-entity-record/index.js';\nexport * from './hooks/use-entity-use-cases/index.js';\nexport * from './hooks/use-wizard/index.js';\nexport * from './query/combine-filtering.js';\nexport * from './query/entity-query-key.js';\nexport * from './query/query-provider.js';\nexport * from './reactive/event-source-channel.js';\nexport * from './reactive/reactive-channel.js';\nexport * from './reactive/use-reactive-invalidation.js';\nexport * from './reactive/use-transaction-settlement.js';\n"],"names":[],"mappings":"AAAA,cAAc,sCAAsC;AACpD,cAAc,oCAAoC;AAClD,cAAc,mCAAmC;AACjD,cAAc,4CAA4C;AAC1D,cAAc,0CAA0C;AACxD,cAAc,uCAAuC;AACrD,cAAc,qCAAqC;AACnD,cAAc,wCAAwC;AACtD,cAAc,8BAA8B;AAC5C,cAAc,+BAA+B;AAC7C,cAAc,8BAA8B;AAC5C,cAAc,4BAA4B;AAC1C,cAAc,qCAAqC;AACnD,cAAc,iCAAiC;AAC/C,cAAc,0CAA0C;AACxD,cAAc,2CAA2C"}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './context/adapters-context/index.js';\nexport * from './hooks/use-data-loading/index.js';\nexport * from './hooks/use-entity-form/index.js';\nexport * from './hooks/use-entity-link-resolver/index.js';\nexport * from './hooks/use-entity-link-source/index.js';\nexport * from './hooks/use-entity-mutation/index.js';\nexport * from './hooks/use-entity-record/index.js';\nexport * from './hooks/use-entity-use-cases/index.js';\nexport * from './hooks/use-wizard/index.js';\nexport * from './query/combine-filtering.js';\nexport * from './query/entity-query-key.js';\nexport * from './query/query-provider.js';\nexport * from './reactive/reactive-channel.js';\nexport * from './reactive/use-reactive-invalidation.js';\nexport * from './reactive/use-transaction-settlement.js';\n"],"names":[],"mappings":"AAAA,cAAc,sCAAsC;AACpD,cAAc,oCAAoC;AAClD,cAAc,mCAAmC;AACjD,cAAc,4CAA4C;AAC1D,cAAc,0CAA0C;AACxD,cAAc,uCAAuC;AACrD,cAAc,qCAAqC;AACnD,cAAc,wCAAwC;AACtD,cAAc,8BAA8B;AAC5C,cAAc,+BAA+B;AAC7C,cAAc,8BAA8B;AAC5C,cAAc,4BAA4B;AAC1C,cAAc,iCAAiC;AAC/C,cAAc,0CAA0C;AACxD,cAAc,2CAA2C"}
@@ -1,6 +1,6 @@
1
- import type { PendingTransactionStore, TransactionRecord } from '@entifix/transactions';
1
+ import type { PendingTransactionStore, TransactionRecord, TransactionStatusReader } from '@entifix/transactions';
2
2
  import type { ReactiveChannel } from './reactive-channel.js';
3
- export interface UseTransactionSettlementOptions {
3
+ interface SettlementBase {
4
4
  /** The stream outcomes arrive on, and whose reconnects trigger a re-query. */
5
5
  channel: ReactiveChannel;
6
6
  /**
@@ -11,10 +11,25 @@ export interface UseTransactionSettlementOptions {
11
11
  * upward. Same seam, and the same reason, as `EntityDraftStore` for autosave.
12
12
  */
13
13
  pending: PendingTransactionStore;
14
- /** Reads one record. `undefined` means *not tracked yet* — never *failed*. */
15
- read: (transactionId: string) => Promise<TransactionRecord | undefined>;
16
14
  pollIntervalMs?: number;
17
15
  }
16
+ /**
17
+ * How a record is read, and exactly one way.
18
+ *
19
+ * `reader` is the port, built once where the host composes its adapters, so the
20
+ * component calling this hook imports no transport. `read` is the same thing as
21
+ * a bare function, for a caller that already holds one.
22
+ */
23
+ type RecordSource = {
24
+ /** Reads one record. `undefined` means *not tracked yet* — never *failed*. */
25
+ reader: TransactionStatusReader;
26
+ read?: never;
27
+ } | {
28
+ /** Reads one record. `undefined` means *not tracked yet* — never *failed*. */
29
+ read: (transactionId: string) => Promise<TransactionRecord | undefined>;
30
+ reader?: never;
31
+ };
32
+ export type UseTransactionSettlementOptions = SettlementBase & RecordSource;
18
33
  /**
19
34
  * Settles the optimistic writes this browser started.
20
35
  *
@@ -37,5 +52,6 @@ export interface UseTransactionSettlementOptions {
37
52
  * un-renders a healthy write at exactly the moment nobody can tell a UI bug from
38
53
  * an outage. Only `FAILED` and `STALE` are terminal.
39
54
  */
40
- export declare function useTransactionSettlement({ channel, pending, read, pollIntervalMs, }: UseTransactionSettlementOptions): void;
55
+ export declare function useTransactionSettlement({ channel, pending, pollIntervalMs, ...source }: UseTransactionSettlementOptions): void;
56
+ export {};
41
57
  //# sourceMappingURL=use-transaction-settlement.d.ts.map
@@ -1 +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"}
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,EACjB,uBAAuB,EACxB,MAAM,uBAAuB,CAAC;AAM/B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAK7D,UAAU,cAAc;IACtB,8EAA8E;IAC9E,OAAO,EAAE,eAAe,CAAC;IACzB;;;;;;OAMG;IACH,OAAO,EAAE,uBAAuB,CAAC;IACjC,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;GAMG;AACH,KAAK,YAAY,GACb;IACE,8EAA8E;IAC9E,MAAM,EAAE,uBAAuB,CAAC;IAChC,IAAI,CAAC,EAAE,KAAK,CAAC;CACd,GACD;IACE,8EAA8E;IAC9E,IAAI,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAAC;IACxE,MAAM,CAAC,EAAE,KAAK,CAAC;CAChB,CAAC;AAEN,MAAM,MAAM,+BAA+B,GAAG,cAAc,GAAG,YAAY,CAAC;AAU5E;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,wBAAwB,CAAC,EACvC,OAAO,EACP,OAAO,EACP,cAAgC,EAChC,GAAG,MAAM,EACV,EAAE,+BAA+B,GAAG,IAAI,CA4GxC"}
@@ -1,8 +1,15 @@
1
1
  'use client';
2
+ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
2
3
  import { useQueryClient } from '@tanstack/react-query';
4
+ import { Effect } from 'effect';
3
5
  import { useCallback, useEffect, useRef } from 'react';
4
6
  import { entityQueryScopeFor } from '../query/entity-query-key.js';
5
7
  /** How often to re-ask while anything is pending. */ const DEFAULT_POLL_MS = 30000;
8
+ const readerOf = (source)=>{
9
+ if (source.reader === undefined) return source.read;
10
+ const { reader } = source;
11
+ return (transactionId)=>Effect.runPromise(reader.read(transactionId));
12
+ };
6
13
  /**
7
14
  * Settles the optimistic writes this browser started.
8
15
  *
@@ -24,7 +31,13 @@ import { entityQueryScopeFor } from '../query/entity-query-key.js';
24
31
  * no event is ever published and the tracker holds nothing. Rolling back there
25
32
  * un-renders a healthy write at exactly the moment nobody can tell a UI bug from
26
33
  * an outage. Only `FAILED` and `STALE` are terminal.
27
- */ export function useTransactionSettlement({ channel, pending, read, pollIntervalMs = DEFAULT_POLL_MS }) {
34
+ */ export function useTransactionSettlement(_0) {
35
+ let { channel, pending, pollIntervalMs = DEFAULT_POLL_MS } = _0, source = _object_without_properties_loose(_0, [
36
+ "channel",
37
+ "pending",
38
+ "pollIntervalMs"
39
+ ]);
40
+ const read = readerOf(source);
28
41
  const queryClient = useQueryClient();
29
42
  // The store is rebuilt whenever an entry changes and `read` is typically
30
43
  // inline, so both are held in refs: keying the subscription on their identity
@@ -1 +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 * (`@entifix/next-shell`, T4) and this hook is T3, 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"}
1
+ {"version":3,"sources":["../../src/reactive/use-transaction-settlement.tsx"],"sourcesContent":["'use client';\n\nimport type {\n PendingEntry,\n PendingTransactionStore,\n TransactionRecord,\n TransactionStatusReader,\n} from '@entifix/transactions';\nimport { useQueryClient } from '@tanstack/react-query';\nimport { Effect } from 'effect';\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\ninterface SettlementBase {\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 * (`@entifix/next-shell`, T4) and this hook is T3, which may not depend\n * upward. Same seam, and the same reason, as `EntityDraftStore` for autosave.\n */\n pending: PendingTransactionStore;\n pollIntervalMs?: number;\n}\n\n/**\n * How a record is read, and exactly one way.\n *\n * `reader` is the port, built once where the host composes its adapters, so the\n * component calling this hook imports no transport. `read` is the same thing as\n * a bare function, for a caller that already holds one.\n */\ntype RecordSource =\n | {\n /** Reads one record. `undefined` means *not tracked yet* — never *failed*. */\n reader: TransactionStatusReader;\n read?: never;\n }\n | {\n /** Reads one record. `undefined` means *not tracked yet* — never *failed*. */\n read: (transactionId: string) => Promise<TransactionRecord | undefined>;\n reader?: never;\n };\n\nexport type UseTransactionSettlementOptions = SettlementBase & RecordSource;\n\nconst readerOf = (\n source: RecordSource,\n): ((transactionId: string) => Promise<TransactionRecord | undefined>) => {\n if (source.reader === undefined) return source.read;\n const { reader } = source;\n return transactionId => Effect.runPromise(reader.read(transactionId));\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 pollIntervalMs = DEFAULT_POLL_MS,\n ...source\n}: UseTransactionSettlementOptions): void {\n const read = readerOf(source);\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","Effect","useCallback","useEffect","useRef","entityQueryScopeFor","DEFAULT_POLL_MS","readerOf","source","reader","undefined","read","transactionId","runPromise","useTransactionSettlement","channel","pending","pollIntervalMs","queryClient","pendingRef","readRef","current","invalidate","entity","invalidateQueries","queryKey","reconcile","entry","record","catch","state","settle","fail","error","reconcileAll","entries","stopListening","subscribe","event","correlationId","find","candidate","name","data","stopWatchingConnections","onConnect","watching","length","timer","setInterval","clearInterval"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"use-entity-load.d.ts","sourceRoot":"","sources":["../../src/url/use-entity-load.ts"],"names":[],"mappings":"AAOA,OAAO,EAEL,KAAK,MAAM,EACX,KAAK,iBAAiB,EACtB,KAAK,UAAU,EAChB,MAAM,eAAe,CAAC;AAIvB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,MAAM,MAAM,UAAU,CAAC,OAAO,SAAS,MAAM,IACzC;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE;AAC7B;;;GAGG;GACD;IAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,CAAA;CAAE,GACvE;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC,OAAO,CAAC,CAAA;CAAE,GAC/D;IAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC;AAiC3D;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,OAAO,SAAS,MAAM,EAClD,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,iBAAiB,CAAC,OAAO,CAAC,GAAG,IAAI,GACzC,UAAU,CAAC,OAAO,CAAC,CA0BrB"}
1
+ {"version":3,"file":"use-entity-load.d.ts","sourceRoot":"","sources":["../../src/url/use-entity-load.ts"],"names":[],"mappings":"AAMA,OAAO,EACL,KAAK,MAAM,EACX,KAAK,iBAAiB,EACtB,KAAK,UAAU,EAChB,MAAM,eAAe,CAAC;AAIvB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,MAAM,MAAM,UAAU,CAAC,OAAO,SAAS,MAAM,IACzC;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE;AAC7B;;;GAGG;GACD;IAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,CAAA;CAAE,GACvE;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC,OAAO,CAAC,CAAA;CAAE,GAC/D;IAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC;AA2B3D;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,OAAO,SAAS,MAAM,EAClD,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,iBAAiB,CAAC,OAAO,CAAC,GAAG,IAAI,GACzC,UAAU,CAAC,OAAO,CAAC,CA0BrB"}
@@ -1,5 +1,4 @@
1
- import { ConfigurationRepositoryTag, EntityLoadRequestTag, EntityRepositoryTag, loadAllUCFactory } from '@entifix/business';
2
- import { ConfigurationClientInMemory } from '@entifix/core';
1
+ import { EntityLoadRequestTag, EntityRepositoryTag, loadAllUCFactory } from '@entifix/business';
3
2
  import { Context, Effect } from 'effect';
4
3
  import { useEffect, useState } from 'react';
5
4
  const IDLE = {
@@ -7,11 +6,9 @@ const IDLE = {
7
6
  };
8
7
  /**
9
8
  * Every record a request keeps, through the `loadAll` use case: a filter
10
- * never pages. A source's repository reads no configuration here, but the
11
- * port leaves `ConfigurationRepositoryTag` on every method's requirement
12
- * channel, so an empty one discharges it.
9
+ * never pages.
13
10
  */ async function loadEvery(repository, request) {
14
- const context = Context.make(EntityRepositoryTag, repository).pipe(Context.add(ConfigurationRepositoryTag, new ConfigurationClientInMemory({})), Context.add(EntityLoadRequestTag, request));
11
+ const context = Context.make(EntityRepositoryTag, repository).pipe(Context.add(EntityLoadRequestTag, request));
15
12
  const items = await Effect.runPromise(Effect.provide(loadAllUCFactory(), context));
16
13
  return {
17
14
  items,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/url/use-entity-load.ts"],"sourcesContent":["import {\n ConfigurationRepositoryTag,\n EntityLoadRequestTag,\n type EntityRepository,\n EntityRepositoryTag,\n loadAllUCFactory,\n} from '@entifix/business';\nimport {\n ConfigurationClientInMemory,\n type Entity,\n type EntityLoadRequest,\n type EntityPage,\n} from '@entifix/core';\nimport { Context, Effect } from 'effect';\nimport { useEffect, useState } from 'react';\n\nimport type { EntitySource } from './entity-source.js';\n\nexport type EntityLoad<TEntity extends Entity> =\n | { readonly status: 'idle' }\n /**\n * Waiting for an answer. `previous` is the last answer from the same\n * source, so a filter being changed keeps showing what it showed.\n */\n | { readonly status: 'pending'; readonly previous?: EntityPage<TEntity> }\n | { readonly status: 'done'; readonly page: EntityPage<TEntity> }\n | { readonly status: 'failed'; readonly error: unknown };\n\nconst IDLE = { status: 'idle' } as const;\n\n/**\n * Every record a request keeps, through the `loadAll` use case: a filter\n * never pages. A source's repository reads no configuration here, but the\n * port leaves `ConfigurationRepositoryTag` on every method's requirement\n * channel, so an empty one discharges it.\n */\nasync function loadEvery<TEntity extends Entity>(\n repository: EntityRepository,\n request: EntityLoadRequest<TEntity>,\n): Promise<EntityPage<TEntity>> {\n const context = Context.make(EntityRepositoryTag, repository).pipe(\n Context.add(\n ConfigurationRepositoryTag,\n new ConfigurationClientInMemory({}),\n ),\n Context.add(EntityLoadRequestTag, request as unknown as EntityLoadRequest),\n );\n const items = await Effect.runPromise(\n Effect.provide(loadAllUCFactory<TEntity>(), context),\n );\n return { items, total: items.length, request };\n}\n\ninterface Answer<TEntity extends Entity> {\n readonly source: EntitySource;\n readonly key: string;\n readonly load: EntityLoad<TEntity>;\n}\n\n/**\n * Every record a request selects, through the `loadAll` use case over a source.\n *\n * `null` asks for nothing and answers `idle`: before hydration, and while no\n * filter is set, the page shows what the build rendered. An answer counts only\n * for the request it was asked for, so a late answer to an older filter is\n * never shown as the current one.\n */\nexport function useEntityLoad<TEntity extends Entity>(\n source: EntitySource,\n request: EntityLoadRequest<TEntity> | null,\n): EntityLoad<TEntity> {\n const [answer, setAnswer] = useState<Answer<TEntity>>();\n // Requests are rebuilt on every render; their content is what changes.\n const key = request === null ? null : JSON.stringify(request);\n\n useEffect(() => {\n if (request === null || key === null) return;\n const settle = (load: EntityLoad<TEntity>) =>\n setAnswer({ source, key, load });\n source()\n .then(repository => loadEvery<TEntity>(repository, request))\n .then(\n page => settle({ status: 'done', page }),\n (error: unknown) => settle({ status: 'failed', error }),\n );\n // `request` is read through `key`, its content: a new object with the\n // same content is the same request.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [source, key]);\n\n if (key === null) return IDLE;\n if (answer?.source !== source) return { status: 'pending' };\n if (answer.key === key) return answer.load;\n return answer.load.status === 'done'\n ? { status: 'pending', previous: answer.load.page }\n : { status: 'pending' };\n}\n"],"names":["ConfigurationRepositoryTag","EntityLoadRequestTag","EntityRepositoryTag","loadAllUCFactory","ConfigurationClientInMemory","Context","Effect","useEffect","useState","IDLE","status","loadEvery","repository","request","context","make","pipe","add","items","runPromise","provide","total","length","useEntityLoad","source","answer","setAnswer","key","JSON","stringify","settle","load","then","page","error","previous"],"mappings":"AAAA,SACEA,0BAA0B,EAC1BC,oBAAoB,EAEpBC,mBAAmB,EACnBC,gBAAgB,QACX,oBAAoB;AAC3B,SACEC,2BAA2B,QAItB,gBAAgB;AACvB,SAASC,OAAO,EAAEC,MAAM,QAAQ,SAAS;AACzC,SAASC,SAAS,EAAEC,QAAQ,QAAQ,QAAQ;AAc5C,MAAMC,OAAO;IAAEC,QAAQ;AAAO;AAE9B;;;;;CAKC,GACD,eAAeC,UACbC,UAA4B,EAC5BC,OAAmC;IAEnC,MAAMC,UAAUT,QAAQU,IAAI,CAACb,qBAAqBU,YAAYI,IAAI,CAChEX,QAAQY,GAAG,CACTjB,4BACA,IAAII,4BAA4B,CAAC,KAEnCC,QAAQY,GAAG,CAAChB,sBAAsBY;IAEpC,MAAMK,QAAQ,MAAMZ,OAAOa,UAAU,CACnCb,OAAOc,OAAO,CAACjB,oBAA6BW;IAE9C,OAAO;QAAEI;QAAOG,OAAOH,MAAMI,MAAM;QAAET;IAAQ;AAC/C;AAQA;;;;;;;CAOC,GACD,OAAO,SAASU,cACdC,MAAoB,EACpBX,OAA0C;IAE1C,MAAM,CAACY,QAAQC,UAAU,GAAGlB;IAC5B,uEAAuE;IACvE,MAAMmB,MAAMd,YAAY,OAAO,OAAOe,KAAKC,SAAS,CAAChB;IAErDN,UAAU;QACR,IAAIM,YAAY,QAAQc,QAAQ,MAAM;QACtC,MAAMG,SAAS,CAACC,OACdL,UAAU;gBAAEF;gBAAQG;gBAAKI;YAAK;QAChCP,SACGQ,IAAI,CAACpB,CAAAA,aAAcD,UAAmBC,YAAYC,UAClDmB,IAAI,CACHC,CAAAA,OAAQH,OAAO;gBAAEpB,QAAQ;gBAAQuB;YAAK,IACtC,CAACC,QAAmBJ,OAAO;gBAAEpB,QAAQ;gBAAUwB;YAAM;IAEzD,sEAAsE;IACtE,oCAAoC;IACpC,uDAAuD;IACzD,GAAG;QAACV;QAAQG;KAAI;IAEhB,IAAIA,QAAQ,MAAM,OAAOlB;IACzB,IAAIgB,CAAAA,0BAAAA,OAAQD,MAAM,MAAKA,QAAQ,OAAO;QAAEd,QAAQ;IAAU;IAC1D,IAAIe,OAAOE,GAAG,KAAKA,KAAK,OAAOF,OAAOM,IAAI;IAC1C,OAAON,OAAOM,IAAI,CAACrB,MAAM,KAAK,SAC1B;QAAEA,QAAQ;QAAWyB,UAAUV,OAAOM,IAAI,CAACE,IAAI;IAAC,IAChD;QAAEvB,QAAQ;IAAU;AAC1B"}
1
+ {"version":3,"sources":["../../src/url/use-entity-load.ts"],"sourcesContent":["import {\n EntityLoadRequestTag,\n type EntityRepository,\n EntityRepositoryTag,\n loadAllUCFactory,\n} from '@entifix/business';\nimport {\n type Entity,\n type EntityLoadRequest,\n type EntityPage,\n} from '@entifix/core';\nimport { Context, Effect } from 'effect';\nimport { useEffect, useState } from 'react';\n\nimport type { EntitySource } from './entity-source.js';\n\nexport type EntityLoad<TEntity extends Entity> =\n | { readonly status: 'idle' }\n /**\n * Waiting for an answer. `previous` is the last answer from the same\n * source, so a filter being changed keeps showing what it showed.\n */\n | { readonly status: 'pending'; readonly previous?: EntityPage<TEntity> }\n | { readonly status: 'done'; readonly page: EntityPage<TEntity> }\n | { readonly status: 'failed'; readonly error: unknown };\n\nconst IDLE = { status: 'idle' } as const;\n\n/**\n * Every record a request keeps, through the `loadAll` use case: a filter\n * never pages.\n */\nasync function loadEvery<TEntity extends Entity>(\n repository: EntityRepository,\n request: EntityLoadRequest<TEntity>,\n): Promise<EntityPage<TEntity>> {\n const context = Context.make(EntityRepositoryTag, repository).pipe(\n Context.add(EntityLoadRequestTag, request as unknown as EntityLoadRequest),\n );\n const items = await Effect.runPromise(\n Effect.provide(loadAllUCFactory<TEntity>(), context),\n );\n return { items, total: items.length, request };\n}\n\ninterface Answer<TEntity extends Entity> {\n readonly source: EntitySource;\n readonly key: string;\n readonly load: EntityLoad<TEntity>;\n}\n\n/**\n * Every record a request selects, through the `loadAll` use case over a source.\n *\n * `null` asks for nothing and answers `idle`: before hydration, and while no\n * filter is set, the page shows what the build rendered. An answer counts only\n * for the request it was asked for, so a late answer to an older filter is\n * never shown as the current one.\n */\nexport function useEntityLoad<TEntity extends Entity>(\n source: EntitySource,\n request: EntityLoadRequest<TEntity> | null,\n): EntityLoad<TEntity> {\n const [answer, setAnswer] = useState<Answer<TEntity>>();\n // Requests are rebuilt on every render; their content is what changes.\n const key = request === null ? null : JSON.stringify(request);\n\n useEffect(() => {\n if (request === null || key === null) return;\n const settle = (load: EntityLoad<TEntity>) =>\n setAnswer({ source, key, load });\n source()\n .then(repository => loadEvery<TEntity>(repository, request))\n .then(\n page => settle({ status: 'done', page }),\n (error: unknown) => settle({ status: 'failed', error }),\n );\n // `request` is read through `key`, its content: a new object with the\n // same content is the same request.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [source, key]);\n\n if (key === null) return IDLE;\n if (answer?.source !== source) return { status: 'pending' };\n if (answer.key === key) return answer.load;\n return answer.load.status === 'done'\n ? { status: 'pending', previous: answer.load.page }\n : { status: 'pending' };\n}\n"],"names":["EntityLoadRequestTag","EntityRepositoryTag","loadAllUCFactory","Context","Effect","useEffect","useState","IDLE","status","loadEvery","repository","request","context","make","pipe","add","items","runPromise","provide","total","length","useEntityLoad","source","answer","setAnswer","key","JSON","stringify","settle","load","then","page","error","previous"],"mappings":"AAAA,SACEA,oBAAoB,EAEpBC,mBAAmB,EACnBC,gBAAgB,QACX,oBAAoB;AAM3B,SAASC,OAAO,EAAEC,MAAM,QAAQ,SAAS;AACzC,SAASC,SAAS,EAAEC,QAAQ,QAAQ,QAAQ;AAc5C,MAAMC,OAAO;IAAEC,QAAQ;AAAO;AAE9B;;;CAGC,GACD,eAAeC,UACbC,UAA4B,EAC5BC,OAAmC;IAEnC,MAAMC,UAAUT,QAAQU,IAAI,CAACZ,qBAAqBS,YAAYI,IAAI,CAChEX,QAAQY,GAAG,CAACf,sBAAsBW;IAEpC,MAAMK,QAAQ,MAAMZ,OAAOa,UAAU,CACnCb,OAAOc,OAAO,CAAChB,oBAA6BU;IAE9C,OAAO;QAAEI;QAAOG,OAAOH,MAAMI,MAAM;QAAET;IAAQ;AAC/C;AAQA;;;;;;;CAOC,GACD,OAAO,SAASU,cACdC,MAAoB,EACpBX,OAA0C;IAE1C,MAAM,CAACY,QAAQC,UAAU,GAAGlB;IAC5B,uEAAuE;IACvE,MAAMmB,MAAMd,YAAY,OAAO,OAAOe,KAAKC,SAAS,CAAChB;IAErDN,UAAU;QACR,IAAIM,YAAY,QAAQc,QAAQ,MAAM;QACtC,MAAMG,SAAS,CAACC,OACdL,UAAU;gBAAEF;gBAAQG;gBAAKI;YAAK;QAChCP,SACGQ,IAAI,CAACpB,CAAAA,aAAcD,UAAmBC,YAAYC,UAClDmB,IAAI,CACHC,CAAAA,OAAQH,OAAO;gBAAEpB,QAAQ;gBAAQuB;YAAK,IACtC,CAACC,QAAmBJ,OAAO;gBAAEpB,QAAQ;gBAAUwB;YAAM;IAEzD,sEAAsE;IACtE,oCAAoC;IACpC,uDAAuD;IACzD,GAAG;QAACV;QAAQG;KAAI;IAEhB,IAAIA,QAAQ,MAAM,OAAOlB;IACzB,IAAIgB,CAAAA,0BAAAA,OAAQD,MAAM,MAAKA,QAAQ,OAAO;QAAEd,QAAQ;IAAU;IAC1D,IAAIe,OAAOE,GAAG,KAAKA,KAAK,OAAOF,OAAOM,IAAI;IAC1C,OAAON,OAAOM,IAAI,CAACrB,MAAM,KAAK,SAC1B;QAAEA,QAAQ;QAAWyB,UAAUV,OAAOM,IAAI,CAACE,IAAI;IAAC,IAChD;QAAEvB,QAAQ;IAAU;AAC1B"}
package/package.json CHANGED
@@ -21,7 +21,7 @@
21
21
  }
22
22
  }
23
23
  },
24
- "version": "0.4.1",
24
+ "version": "0.5.0",
25
25
  "description": "React hooks that bind entities to data: TanStack Query and Form over the entity adapters, drafts, mutations and live invalidation.",
26
26
  "license": "MIT",
27
27
  "repository": {
@@ -47,6 +47,11 @@
47
47
  "types": "./dist/url.d.ts",
48
48
  "import": "./dist/url.js",
49
49
  "default": "./dist/url.js"
50
+ },
51
+ "./event-source": {
52
+ "types": "./dist/event-source.d.ts",
53
+ "import": "./dist/event-source.js",
54
+ "default": "./dist/event-source.js"
50
55
  }
51
56
  },
52
57
  "files": [
@@ -55,16 +60,16 @@
55
60
  "LICENSE"
56
61
  ],
57
62
  "dependencies": {
58
- "@entifix/business": "0.4.1",
59
- "@entifix/core": "0.4.1",
60
- "@entifix/transactions": "0.4.1"
63
+ "@entifix/business": "0.5.0",
64
+ "@entifix/core": "0.5.0",
65
+ "@entifix/transactions": "0.5.0"
61
66
  },
62
67
  "devDependencies": {
63
68
  "@tanstack/react-form": "1.33.5",
64
69
  "@tanstack/react-query": "^5.104.5",
65
70
  "effect": "4.0.5",
66
71
  "react": "19.3.1",
67
- "@entifix/testing-unit": "0.4.1"
72
+ "@entifix/testing-unit": "0.5.0"
68
73
  },
69
74
  "peerDependencies": {
70
75
  "@tanstack/react-form": ">=1.33 <2",