akanjs 3.0.0-alpha.71 → 3.0.0-alpha.73
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/client/cookie.ts +21 -34
- package/client/csrTypes.ts +0 -12
- package/client/frameConfig.ts +13 -0
- package/common/Logger.ts +13 -6
- package/common/TrustedProxy.ts +134 -0
- package/common/clamp.ts +2 -0
- package/common/dynamicRecord.ts +12 -0
- package/common/index.ts +5 -1
- package/common/isValidDate.ts +1 -1
- package/common/requestOrigin.ts +32 -0
- package/common/{httpClient.ts → restClient.ts} +16 -9
- package/constant/types.ts +22 -0
- package/dictionary/base.dictionary.ts +4 -0
- package/dictionary/trans.ts +24 -5
- package/document/dataLoader.ts +0 -4
- package/document/filterMeta.ts +2 -2
- package/document/loaderInfo.ts +4 -1
- package/fetch/client/fetchClient.ts +65 -88
- package/fetch/client/fetchHandle.ts +57 -0
- package/fetch/client/httpClient.ts +42 -14
- package/fetch/client/sliceInitHandle.ts +101 -0
- package/fetch/fetchType/appliedReturn.type.ts +64 -1
- package/fetch/fetchType/sliceFetch.type.ts +5 -5
- package/fetch/requestStorage.ts +19 -5
- package/index.ts +16 -2
- package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
- package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
- package/package.json +1 -1
- package/server/akanApp.ts +3 -19
- package/server/akanAppHeaders.ts +10 -4
- package/server/akanOption.ts +14 -1
- package/server/akanServer.ts +3 -0
- package/server/cachePolicy.ts +2 -11
- package/server/decorators.ts +12 -3
- package/server/devtools/constantSerializer.ts +8 -1
- package/server/di/diLifecycle.ts +21 -1
- package/server/di/resolveHierarchy.ts +3 -3
- package/server/imageOptimizer.ts +2 -17
- package/server/mcp/McpDispatcher.ts +8 -2
- package/server/resolver/database.resolver.ts +13 -8
- package/server/resolver/signal.resolver.ts +3 -3
- package/server/routeTreeBuilder.ts +1 -15
- package/server/routing/apiRouter.ts +4 -5
- package/server/routing/appWsData.ts +9 -6
- package/server/staticPath.ts +34 -0
- package/server/webRouter.ts +35 -41
- package/service/adapt.ts +1 -5
- package/service/predefinedAdaptor/cache.adaptor.ts +8 -4
- package/service/predefinedAdaptor/database.adaptor.ts +37 -1766
- package/service/predefinedAdaptor/sql/PendingStoreEnsures.ts +22 -0
- package/service/predefinedAdaptor/sql/QueryCompiler.ts +225 -0
- package/service/predefinedAdaptor/sql/SqlDocumentStore.ts +812 -0
- package/service/predefinedAdaptor/sql/UpdateCompiler.ts +67 -0
- package/service/predefinedAdaptor/sql/dialect/postgres.ts +151 -0
- package/service/predefinedAdaptor/sql/dialect/sqlite.ts +134 -0
- package/service/predefinedAdaptor/sql/driver/bunSqlite.ts +29 -0
- package/service/predefinedAdaptor/sql/driver/libsql.ts +37 -0
- package/service/predefinedAdaptor/sql/driver/postgres.ts +42 -0
- package/service/predefinedAdaptor/sql/types.ts +251 -0
- package/service/predefinedAdaptor/sql/values.ts +91 -0
- package/signal/CrossSiteGuard.ts +71 -0
- package/signal/SignalFailure.ts +51 -0
- package/signal/endpoint.ts +2 -2
- package/signal/exception.ts +43 -6
- package/signal/index.ts +2 -0
- package/signal/intercept.ts +5 -2
- package/signal/internal.ts +10 -2
- package/signal/serializer/fetch.serializer.ts +11 -1
- package/signal/signalContext.ts +25 -28
- package/signal/slice.ts +1 -1
- package/store/SliceRequest.ts +23 -0
- package/store/action.ts +177 -114
- package/store/agent/AgentBridge.ts +3 -1
- package/store/agentic/StExposeBuilder.ts +7 -4
- package/store/agentic/StStateBuilder.ts +9 -4
- package/store/agentic/StToolBuilder.ts +21 -7
- package/store/store.ts +1 -1
- package/store/storeInstance.ts +3 -3
- package/store/storeRegistry.ts +1 -1
- package/types/client/cookie.d.ts +13 -2
- package/types/client/csrTypes.d.ts +0 -1
- package/types/client/frameConfig.d.ts +1 -0
- package/types/common/TrustedProxy.d.ts +24 -0
- package/types/common/clamp.d.ts +2 -0
- package/types/common/dynamicRecord.d.ts +14 -0
- package/types/common/index.d.ts +5 -1
- package/types/common/requestOrigin.d.ts +14 -0
- package/types/common/restClient.d.ts +27 -0
- package/types/constant/types.d.ts +13 -0
- package/types/dictionary/base.dictionary.d.ts +1 -1
- package/types/dictionary/dictionary.d.ts +8 -8
- package/types/document/dataLoader.d.ts +0 -3
- package/types/document/filterMeta.d.ts +2 -2
- package/types/document/loaderInfo.d.ts +2 -2
- package/types/fetch/client/fetchHandle.d.ts +24 -0
- package/types/fetch/client/httpClient.d.ts +2 -0
- package/types/fetch/client/sliceInitHandle.d.ts +29 -0
- package/types/fetch/fetchType/appliedReturn.type.d.ts +38 -1
- package/types/fetch/fetchType/sliceFetch.type.d.ts +5 -5
- package/types/fetch/requestStorage.d.ts +14 -0
- package/types/index.d.ts +17 -2
- package/types/server/akanOption.d.ts +8 -1
- package/types/server/decorators.d.ts +12 -2
- package/types/server/di/diLifecycle.d.ts +9 -1
- package/types/server/routeTreeBuilder.d.ts +1 -2
- package/types/server/routing/appWsData.d.ts +7 -3
- package/types/server/staticPath.d.ts +17 -0
- package/types/service/adapt.d.ts +1 -1
- package/types/service/predefinedAdaptor/database.adaptor.d.ts +15 -407
- package/types/service/predefinedAdaptor/sql/PendingStoreEnsures.d.ts +6 -0
- package/types/service/predefinedAdaptor/sql/QueryCompiler.d.ts +28 -0
- package/types/service/predefinedAdaptor/sql/SqlDocumentStore.d.ts +127 -0
- package/types/service/predefinedAdaptor/sql/UpdateCompiler.d.ts +17 -0
- package/types/service/predefinedAdaptor/sql/dialect/postgres.d.ts +27 -0
- package/types/service/predefinedAdaptor/sql/dialect/sqlite.d.ts +27 -0
- package/types/service/predefinedAdaptor/sql/driver/bunSqlite.d.ts +16 -0
- package/types/service/predefinedAdaptor/sql/driver/libsql.d.ts +17 -0
- package/types/service/predefinedAdaptor/sql/driver/postgres.d.ts +17 -0
- package/types/service/predefinedAdaptor/sql/types.d.ts +226 -0
- package/types/service/predefinedAdaptor/sql/values.d.ts +13 -0
- package/types/signal/CrossSiteGuard.d.ts +44 -0
- package/types/signal/SignalFailure.d.ts +28 -0
- package/types/signal/exception.d.ts +38 -54774
- package/types/signal/index.d.ts +2 -0
- package/types/signal/intercept.d.ts +3 -3
- package/types/signal/internal.d.ts +5 -1
- package/types/signal/serializer/fetch.serializer.d.ts +7 -1
- package/types/store/SliceRequest.d.ts +16 -0
- package/types/store/agentic/StToolBuilder.d.ts +10 -1
- package/types/ui/Agent/Menu.d.ts +1 -1
- package/types/ui/Field/Choice.d.ts +52 -0
- package/types/ui/Field/Collection.d.ts +51 -0
- package/types/ui/Field/DateField.d.ts +32 -0
- package/types/ui/Field/Label.d.ts +9 -0
- package/types/ui/Field/NumberField.d.ts +43 -0
- package/types/ui/Field/Relation.d.ts +80 -0
- package/types/ui/Field/Text.d.ts +101 -0
- package/types/ui/Field/index.d.ts +50 -0
- package/types/ui/Field.d.ts +1 -412
- package/types/ui/Load/Edit_Client.d.ts +3 -1
- package/types/ui/Load/Stream.d.ts +22 -0
- package/types/ui/Load/index.d.ts +2 -0
- package/types/ui/System/Root.d.ts +4 -0
- package/types/ui/index.d.ts +1 -1
- package/types/ui/recipe/badgeRecipe.d.ts +2 -2
- package/types/ui/recipe/buttonRecipe.d.ts +2 -2
- package/types/webkit/useCamera.d.ts +10 -1
- package/types/webkit/usePurchase.d.ts +4 -0
- package/ui/Agent/Menu.tsx +2 -2
- package/ui/Data/TableList.tsx +1 -1
- package/ui/DraggableList.tsx +1 -2
- package/ui/Field/Choice.tsx +172 -0
- package/ui/Field/Collection.tsx +293 -0
- package/ui/Field/DateField.tsx +138 -0
- package/ui/Field/Label.tsx +31 -0
- package/ui/Field/NumberField.tsx +169 -0
- package/ui/Field/Relation.tsx +387 -0
- package/ui/Field/Text.tsx +360 -0
- package/ui/Field/index.tsx +70 -0
- package/ui/Input.tsx +3 -3
- package/ui/Load/Edit.tsx +2 -2
- package/ui/Load/Edit_Client.tsx +11 -3
- package/ui/Load/Pagination.tsx +25 -25
- package/ui/Load/Stream.tsx +38 -0
- package/ui/Load/Units.tsx +50 -37
- package/ui/Load/View.tsx +25 -17
- package/ui/Load/index.ts +2 -0
- package/ui/Model/EditModal.tsx +7 -7
- package/ui/Signal/Message.tsx +7 -7
- package/ui/Signal/PubSub.tsx +4 -1
- package/ui/System/Client.tsx +0 -2
- package/ui/System/Gtag.tsx +1 -1
- package/ui/System/Root.tsx +4 -0
- package/ui/index.ts +1 -1
- package/ui/overlayPosition.ts +1 -2
- package/webkit/useCamera.tsx +22 -6
- package/webkit/useCodepush.tsx +2 -2
- package/webkit/useCsrValues.ts +1 -3
- package/webkit/useFetch.ts +4 -3
- package/webkit/useFrameRuntime.ts +14 -7
- package/webkit/usePurchase.tsx +4 -1
- package/fetch/serializer/fetch.serializer.ts +0 -142
- package/fetch/serializer/index.ts +0 -1
- package/types/common/httpClient.d.ts +0 -20
- package/types/fetch/serializer/fetch.serializer.d.ts +0 -14
- package/types/fetch/serializer/index.d.ts +0 -1
- package/types/fetch/serializer.d.ts +0 -1
- package/ui/Field.tsx +0 -1639
|
@@ -1,13 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getEnv, PrimitiveRegistry, type PromiseOrObject } from "akanjs/base";
|
|
2
2
|
import { capitalize, type FetchPolicy, fileUploadContract, Logger, resolveFileUploadCapability } from "akanjs/common";
|
|
3
|
-
import {
|
|
4
|
-
type BaseInsight,
|
|
5
|
-
type BaseObject,
|
|
6
|
-
ConstantRegistry,
|
|
7
|
-
deserialize,
|
|
8
|
-
serialize,
|
|
9
|
-
withSharedInstances,
|
|
10
|
-
} from "akanjs/constant";
|
|
3
|
+
import { type BaseInsight, type BaseObject, ConstantRegistry, deserialize, serialize } from "akanjs/constant";
|
|
11
4
|
import type {
|
|
12
5
|
DatabaseSignal,
|
|
13
6
|
SerializedArg,
|
|
@@ -19,14 +12,11 @@ import type {
|
|
|
19
12
|
} from "akanjs/signal";
|
|
20
13
|
import { agentTurnConstant } from "../agentTurn";
|
|
21
14
|
import type { ClientSignal, FetchClientType, FetchSignalInput, MergeAllFetchTypes, SliceMeta } from "../fetchType";
|
|
22
|
-
import {
|
|
23
|
-
getRequestStore,
|
|
24
|
-
memoizeRequestQuery,
|
|
25
|
-
cookies as requestCookies,
|
|
26
|
-
headers as requestHeaders,
|
|
27
|
-
} from "../requestStorage";
|
|
15
|
+
import { claimRequestQuery, cookies as requestCookies, headers as requestHeaders } from "../requestStorage";
|
|
28
16
|
import type { GetSliceMetaObjFromDatabaseSignals } from "../types";
|
|
17
|
+
import { FetchHandle } from "./fetchHandle";
|
|
29
18
|
import { type ErrorConstructor, HttpClient } from "./httpClient";
|
|
19
|
+
import { SliceInitHandle } from "./sliceInitHandle";
|
|
30
20
|
import { WsClient } from "./wsClient";
|
|
31
21
|
|
|
32
22
|
type FetchHandler = (...args: unknown[]) => PromiseOrObject<unknown>;
|
|
@@ -313,11 +303,12 @@ export class FetchClient {
|
|
|
313
303
|
const baseUrl = option?.origin;
|
|
314
304
|
|
|
315
305
|
const requestQuery = () => this.http.get(url, { headers, baseUrl });
|
|
316
|
-
const response = baseUrl
|
|
317
|
-
? await requestQuery()
|
|
318
|
-
: await memoizeRequestQuery(FetchClient.#makeRequestQueryCacheKey(this.origin, url, headers), requestQuery);
|
|
319
306
|
|
|
320
|
-
const
|
|
307
|
+
const claim = baseUrl
|
|
308
|
+
? { value: requestQuery(), owned: true }
|
|
309
|
+
: claimRequestQuery(FetchClient.#makeRequestQueryCacheKey(this.origin, url, headers), requestQuery);
|
|
310
|
+
const response = await claim.value;
|
|
311
|
+
const payload = claim.owned ? response : FetchClient.#deepCopy(response);
|
|
321
312
|
return parseReturn(payload, { crystalize: option?.crystalize ?? true });
|
|
322
313
|
};
|
|
323
314
|
return queryFn;
|
|
@@ -525,19 +516,8 @@ export class FetchClient {
|
|
|
525
516
|
const anyCruGuards = signal.cruGuards ?? signal.createGuards ?? signal.updateGuards ?? signal.removeGuards;
|
|
526
517
|
const updateGuards = signal.updateGuards ?? signal.cruGuards;
|
|
527
518
|
if (anyCruGuards) {
|
|
528
|
-
this.#setHandlerFactory(
|
|
529
|
-
names.viewModel,
|
|
530
|
-
() =>
|
|
531
|
-
(async (id: string, option?: FetchPolicy) => {
|
|
532
|
-
const cnst = ConstantRegistry.getDatabase(refName);
|
|
533
|
-
const modelFn = this.#requireHandler(names.model, names.viewModel);
|
|
534
|
-
const modelObj = await modelFn(id, { ...option, crystalize: false });
|
|
535
|
-
const model = new cnst.full(modelObj as object);
|
|
536
|
-
return {
|
|
537
|
-
[refName]: model,
|
|
538
|
-
[`${refName}View`]: { refName, [`${refName}Obj`]: modelObj, [`${refName}ViewAt`]: new Date() },
|
|
539
|
-
};
|
|
540
|
-
}) as FetchHandler,
|
|
519
|
+
this.#setHandlerFactory(names.viewModel, () =>
|
|
520
|
+
this.#makeModelHandleFn(refName, names.model, names.viewModel, `${refName}View`),
|
|
541
521
|
);
|
|
542
522
|
this.#setHandlerFactory(
|
|
543
523
|
names.getModelView,
|
|
@@ -548,19 +528,8 @@ export class FetchClient {
|
|
|
548
528
|
return { refName, [`${refName}Obj`]: modelObj, [`${refName}ViewAt`]: new Date() };
|
|
549
529
|
}) as FetchHandler,
|
|
550
530
|
);
|
|
551
|
-
this.#setHandlerFactory(
|
|
552
|
-
names.editModel,
|
|
553
|
-
() =>
|
|
554
|
-
(async (id: string, option?: FetchPolicy) => {
|
|
555
|
-
const cnst = ConstantRegistry.getDatabase(refName);
|
|
556
|
-
const modelFn = this.#requireHandler(names.model, names.editModel);
|
|
557
|
-
const modelObj = await modelFn(id, { ...option, crystalize: false });
|
|
558
|
-
const model = new cnst.full(modelObj as object);
|
|
559
|
-
return {
|
|
560
|
-
[refName]: model,
|
|
561
|
-
[`${refName}Edit`]: { refName, [`${refName}Obj`]: modelObj, [`${refName}ViewAt`]: new Date() },
|
|
562
|
-
};
|
|
563
|
-
}) as FetchHandler,
|
|
531
|
+
this.#setHandlerFactory(names.editModel, () =>
|
|
532
|
+
this.#makeModelHandleFn(refName, names.model, names.editModel, `${refName}Edit`),
|
|
564
533
|
);
|
|
565
534
|
this.#setHandlerFactory(
|
|
566
535
|
names.getModelEdit,
|
|
@@ -606,6 +575,35 @@ export class FetchClient {
|
|
|
606
575
|
);
|
|
607
576
|
}
|
|
608
577
|
|
|
578
|
+
/**
|
|
579
|
+
* `view<Model>` and `edit<Model>` differ only in the key their payload lands under, so they share one read.
|
|
580
|
+
* The full model is built lazily for the same reason a slice's list is: a route that only hands the payload to
|
|
581
|
+
* `Load.View` never reads it, and `ViewAt` is stamped once so two reads agree.
|
|
582
|
+
*/
|
|
583
|
+
#makeModelHandleFn(refName: string, modelKey: string, callerKey: string, payloadKey: string) {
|
|
584
|
+
return ((id: string, option?: FetchPolicy) => {
|
|
585
|
+
const cnst = ConstantRegistry.getDatabase(refName);
|
|
586
|
+
const modelFn = this.#requireHandler(modelKey, callerKey);
|
|
587
|
+
const modelRequest = modelFn(id, { ...option, crystalize: false }) as Promise<object>;
|
|
588
|
+
let model: object | undefined;
|
|
589
|
+
let payload: object | undefined;
|
|
590
|
+
const modelOf = (modelObj: object) => (model ??= new cnst.full(modelObj));
|
|
591
|
+
const payloadOf = (modelObj: object) =>
|
|
592
|
+
(payload ??= { refName, [`${refName}Obj`]: modelObj, [`${refName}ViewAt`]: new Date() });
|
|
593
|
+
return FetchHandle.of<Record<string, unknown>, Record<string, Promise<unknown>>>(
|
|
594
|
+
[modelRequest],
|
|
595
|
+
async () => {
|
|
596
|
+
const modelObj = await modelRequest;
|
|
597
|
+
return FetchHandle.lazy({}, { [refName]: () => modelOf(modelObj), [payloadKey]: () => payloadOf(modelObj) });
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
[refName]: () => modelRequest.then(modelOf),
|
|
601
|
+
[payloadKey]: () => modelRequest.then(payloadOf),
|
|
602
|
+
},
|
|
603
|
+
);
|
|
604
|
+
}) as FetchHandler;
|
|
605
|
+
}
|
|
606
|
+
|
|
609
607
|
static getEndpointFromSlice(
|
|
610
608
|
refName: string,
|
|
611
609
|
suffix: string,
|
|
@@ -654,7 +652,7 @@ export class FetchClient {
|
|
|
654
652
|
|
|
655
653
|
const argLength = slice.args.length;
|
|
656
654
|
this.slice[sliceName] = { refName, sliceName, argLength };
|
|
657
|
-
this.#setHandlerFactory(names.init, () =>
|
|
655
|
+
this.#setHandlerFactory(names.init, () => (...argData: unknown[]) => {
|
|
658
656
|
const cnst = ConstantRegistry.getDatabase(refName);
|
|
659
657
|
const queryArgs = normalizeQueryArgs(
|
|
660
658
|
Array.from({ length: Math.min(argData.length, argLength) }, (_, idx) => argData[idx]),
|
|
@@ -667,56 +665,35 @@ export class FetchClient {
|
|
|
667
665
|
const listFn = this.#requireHandler<(...args: unknown[]) => Promise<unknown[]>>(names.list, names.init);
|
|
668
666
|
const insightFn = this.#requireHandler<(...args: unknown[]) => Promise<unknown>>(names.insight, names.init);
|
|
669
667
|
|
|
670
|
-
const
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
const
|
|
675
|
-
?
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
const serverInit = {
|
|
668
|
+
const listRequest = listFn(...fetchQueryArgs, skip, limit, sort, {
|
|
669
|
+
...option,
|
|
670
|
+
crystalize: false,
|
|
671
|
+
}) as Promise<BaseObject[]>;
|
|
672
|
+
const insightRequest = (
|
|
673
|
+
fetchInsight ? insightFn(...fetchQueryArgs, { ...option, crystalize: false }) : Promise.resolve(null)
|
|
674
|
+
) as Promise<BaseInsight | null>;
|
|
675
|
+
return new SliceInitHandle({
|
|
679
676
|
refName,
|
|
677
|
+
capRefName,
|
|
678
|
+
capSuffix,
|
|
680
679
|
sliceName,
|
|
681
680
|
argLength,
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
};
|
|
691
|
-
|
|
692
|
-
let modelList: DataList<BaseObject> | undefined;
|
|
693
|
-
let modelInsight: object | undefined;
|
|
694
|
-
return Object.defineProperties({ [`${refName}Init${capSuffix}`]: serverInit } as Record<string, unknown>, {
|
|
695
|
-
[`${refName}List${capSuffix}`]: {
|
|
696
|
-
enumerable: true,
|
|
697
|
-
get: () => {
|
|
698
|
-
modelList ??= new DataList(
|
|
699
|
-
withSharedInstances(() => modelObjList.map((modelObj) => new cnst.light(modelObj) as BaseObject)),
|
|
700
|
-
);
|
|
701
|
-
return modelList;
|
|
702
|
-
},
|
|
703
|
-
},
|
|
704
|
-
[`${refName}Insight${capSuffix}`]: {
|
|
705
|
-
enumerable: true,
|
|
706
|
-
get: () => {
|
|
707
|
-
modelInsight ??= new cnst.insight(modelObjInsight);
|
|
708
|
-
return modelInsight;
|
|
709
|
-
},
|
|
710
|
-
},
|
|
711
|
-
});
|
|
681
|
+
queryArgs,
|
|
682
|
+
page,
|
|
683
|
+
limit,
|
|
684
|
+
sort,
|
|
685
|
+
listRequest,
|
|
686
|
+
insightRequest,
|
|
687
|
+
light: cnst.light as unknown as new (obj?: unknown) => BaseObject,
|
|
688
|
+
insight: cnst.insight as unknown as new (obj?: unknown) => object,
|
|
689
|
+
}).build();
|
|
712
690
|
});
|
|
713
691
|
this.#setHandlerFactory(names.getInit, () => async (...args: unknown[]) => {
|
|
714
|
-
const initFn = this.#requireHandler<(...args: unknown[]) =>
|
|
692
|
+
const initFn = this.#requireHandler<(...args: unknown[]) => Record<string, Promise<unknown>>>(
|
|
715
693
|
names.init,
|
|
716
694
|
names.getInit,
|
|
717
695
|
);
|
|
718
|
-
|
|
719
|
-
return result[`${refName}Init${capSuffix}`];
|
|
696
|
+
return await initFn(...args)[`${refName}Init${capSuffix}`];
|
|
720
697
|
});
|
|
721
698
|
}
|
|
722
699
|
#makeArgSerializer(args: SerializedArg[]) {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
type FieldFactories<Handle> = { [Key in keyof Handle]: () => Handle[Key] };
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A fetch result that is both awaitable and destructurable.
|
|
5
|
+
*
|
|
6
|
+
* `await` gives the object the helper has always given, so every existing call site reads unchanged. Reading a
|
|
7
|
+
* field off the un-awaited result gives that field's own promise instead, so a route can hand each one to the
|
|
8
|
+
* section that renders it and let the slowest arrive last rather than holding the whole page.
|
|
9
|
+
*/
|
|
10
|
+
export class FetchHandle {
|
|
11
|
+
/**
|
|
12
|
+
* @param requests the calls already in flight. Each gets a swallowing handler so a field nobody reads cannot
|
|
13
|
+
* surface as an unhandled rejection; the handle's own `then` is what reports one.
|
|
14
|
+
* @param settle builds the awaited shape — its own fields stay lazy, so awaiting costs nothing extra.
|
|
15
|
+
* @param fields one factory per field of the un-awaited shape, memoized on first read.
|
|
16
|
+
*/
|
|
17
|
+
static of<Awaited extends object, Handle extends object>(
|
|
18
|
+
requests: Promise<unknown>[],
|
|
19
|
+
settle: () => Promise<Awaited>,
|
|
20
|
+
fields: FieldFactories<Handle>,
|
|
21
|
+
): PromiseLike<Awaited> & Handle {
|
|
22
|
+
for (const request of requests) void request.catch(() => undefined);
|
|
23
|
+
let settled: Promise<Awaited> | undefined;
|
|
24
|
+
const resolve = () => {
|
|
25
|
+
if (settled) return settled;
|
|
26
|
+
settled = settle();
|
|
27
|
+
void settled.catch(() => undefined);
|
|
28
|
+
return settled;
|
|
29
|
+
};
|
|
30
|
+
const read = new Map<string, unknown>();
|
|
31
|
+
const factories = fields as { [key: string]: () => unknown };
|
|
32
|
+
const handle = {} as { [key: string]: unknown };
|
|
33
|
+
for (const key of Object.keys(factories)) {
|
|
34
|
+
Object.defineProperty(handle, key, {
|
|
35
|
+
enumerable: true,
|
|
36
|
+
get: () => {
|
|
37
|
+
if (!read.has(key)) read.set(key, factories[key]());
|
|
38
|
+
return read.get(key);
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return Object.defineProperties(handle, {
|
|
44
|
+
|
|
45
|
+
then: { value: (...args: unknown[]) => Reflect.apply(resolve().then, resolve(), args) },
|
|
46
|
+
catch: { value: (...args: unknown[]) => Reflect.apply(resolve().catch, resolve(), args) },
|
|
47
|
+
finally: { value: (...args: unknown[]) => Reflect.apply(resolve().finally, resolve(), args) },
|
|
48
|
+
}) as PromiseLike<Awaited> & Handle;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The awaited shape's fields stay lazy for the same reason the handle's do: a caller reads one, not all. */
|
|
52
|
+
static lazy<Target extends object>(target: Target, fields: { [key: string]: () => unknown }): Target {
|
|
53
|
+
for (const [key, factory] of Object.entries(fields))
|
|
54
|
+
Object.defineProperty(target, key, { enumerable: true, get: factory });
|
|
55
|
+
return target;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -25,6 +25,8 @@ export interface ErrorConstructor {
|
|
|
25
25
|
interface FetchOptions {
|
|
26
26
|
headers?: Record<string, string>;
|
|
27
27
|
baseUrl?: string;
|
|
28
|
+
/** Milliseconds before the request is abandoned; `false` waits as long as the browser will. */
|
|
29
|
+
timeout?: number | false;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
const jsonContentType = /^application\/(?:[\w.+-]+\+)?json\b/i;
|
|
@@ -39,6 +41,15 @@ const transportErrorKeyMap = {
|
|
|
39
41
|
const serverUnreachableKey = "base.error.serverUnreachable";
|
|
40
42
|
const unexpectedResponseKey = "base.error.unexpectedResponse";
|
|
41
43
|
const transportDetailLimit = 200;
|
|
44
|
+
/**
|
|
45
|
+
* How long a call waits before giving up.
|
|
46
|
+
*
|
|
47
|
+
* Nothing else bounds it: a gateway answers a dead upstream with a 504, but a solo process has no gateway and a
|
|
48
|
+
* severed connection produces no response at all — the request then sits until the browser's own limit, which is
|
|
49
|
+
* minutes. The dictionary already has the wording for a slow server (`gatewayTimeout`), so this is the client
|
|
50
|
+
* side of an answer that only existed for one deployment shape.
|
|
51
|
+
*/
|
|
52
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
42
53
|
|
|
43
54
|
export class HttpClient {
|
|
44
55
|
readonly baseUrl: string;
|
|
@@ -63,9 +74,11 @@ export class HttpClient {
|
|
|
63
74
|
return { Accept: "application/json", ...headers, ...options.headers };
|
|
64
75
|
}
|
|
65
76
|
async get<Returns = unknown>(url: string, options: FetchOptions = {}): Promise<Returns> {
|
|
66
|
-
return await this.#request<Returns>(
|
|
67
|
-
|
|
68
|
-
|
|
77
|
+
return await this.#request<Returns>(
|
|
78
|
+
this.#resolveUrl(url, options),
|
|
79
|
+
{ headers: HttpClient.#makeHeaders({ "Content-Type": "application/json" }, options) },
|
|
80
|
+
options,
|
|
81
|
+
);
|
|
69
82
|
}
|
|
70
83
|
#makeReqContent(data: FormData | Record<string, unknown>): { body: BodyInit; headers: Record<string, string> } {
|
|
71
84
|
|
|
@@ -79,11 +92,11 @@ export class HttpClient {
|
|
|
79
92
|
options: FetchOptions = {},
|
|
80
93
|
): Promise<Returns> {
|
|
81
94
|
const { body, headers } = this.#makeReqContent(data);
|
|
82
|
-
return await this.#request<Returns>(
|
|
83
|
-
|
|
84
|
-
body,
|
|
85
|
-
|
|
86
|
-
|
|
95
|
+
return await this.#request<Returns>(
|
|
96
|
+
this.#resolveUrl(url, options),
|
|
97
|
+
{ method, body, headers: HttpClient.#makeHeaders(headers, options) },
|
|
98
|
+
options,
|
|
99
|
+
);
|
|
87
100
|
}
|
|
88
101
|
async put<Returns = unknown>(
|
|
89
102
|
url: string,
|
|
@@ -100,17 +113,29 @@ export class HttpClient {
|
|
|
100
113
|
return await this.send<Returns>("POST", url, data, options);
|
|
101
114
|
}
|
|
102
115
|
async delete<Returns = unknown>(url: string, options: FetchOptions = {}): Promise<Returns> {
|
|
103
|
-
return await this.#request<Returns>(
|
|
104
|
-
|
|
105
|
-
headers: HttpClient.#makeHeaders({ "Content-Type": "application/json" }, options),
|
|
106
|
-
|
|
116
|
+
return await this.#request<Returns>(
|
|
117
|
+
this.#resolveUrl(url, options),
|
|
118
|
+
{ method: "DELETE", headers: HttpClient.#makeHeaders({ "Content-Type": "application/json" }, options) },
|
|
119
|
+
options,
|
|
120
|
+
);
|
|
107
121
|
}
|
|
108
122
|
|
|
109
|
-
async #request<Returns>(url: string, init: RequestInit): Promise<Returns> {
|
|
110
|
-
const res = await this.#fetch(url, init);
|
|
123
|
+
async #request<Returns>(url: string, init: RequestInit, options: FetchOptions = {}): Promise<Returns> {
|
|
124
|
+
const res = await this.#fetch(url, HttpClient.#withTimeout(init, options));
|
|
111
125
|
return await this.#readJsonResponse<Returns>(res);
|
|
112
126
|
}
|
|
113
127
|
|
|
128
|
+
/**
|
|
129
|
+
* An upload gets no deadline of its own: a large file on a slow uplink is a long request that is working, and
|
|
130
|
+
* the request body is the only thing this side can tell that from. Everything else takes the default unless
|
|
131
|
+
* the caller named one.
|
|
132
|
+
*/
|
|
133
|
+
static #withTimeout(init: RequestInit, options: FetchOptions): RequestInit {
|
|
134
|
+
const timeout = options.timeout ?? (init.body instanceof FormData ? false : DEFAULT_TIMEOUT_MS);
|
|
135
|
+
if (timeout === false || !Number.isFinite(timeout) || timeout <= 0) return init;
|
|
136
|
+
return { ...init, signal: AbortSignal.timeout(timeout) };
|
|
137
|
+
}
|
|
138
|
+
|
|
114
139
|
/**
|
|
115
140
|
* `fetch` rejects only when no response arrived at all — a refused connection, a DNS failure, a socket
|
|
116
141
|
* dropped mid-flight. That is the server being unreachable, not an error this API reported, so it is
|
|
@@ -120,6 +145,9 @@ export class HttpClient {
|
|
|
120
145
|
try {
|
|
121
146
|
return await fetch(url, init);
|
|
122
147
|
} catch (error) {
|
|
148
|
+
|
|
149
|
+
if (error instanceof Error && error.name === "TimeoutError")
|
|
150
|
+
throw this.#restoreError({ error: transportErrorKeyMap[408], data: { status: 408 } }, 408);
|
|
123
151
|
if (error instanceof Error && error.name === "AbortError") throw error;
|
|
124
152
|
throw this.#restoreError({ error: serverUnreachableKey, details: String(error) }, 503);
|
|
125
153
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { DataList } from "akanjs/base";
|
|
2
|
+
import { type BaseInsight, type BaseObject, withSharedInstances } from "akanjs/constant";
|
|
3
|
+
|
|
4
|
+
import { FetchHandle } from "./fetchHandle";
|
|
5
|
+
|
|
6
|
+
export interface SliceInitInput {
|
|
7
|
+
refName: string;
|
|
8
|
+
capRefName: string;
|
|
9
|
+
capSuffix: string;
|
|
10
|
+
sliceName: string;
|
|
11
|
+
argLength: number;
|
|
12
|
+
queryArgs: unknown[];
|
|
13
|
+
page: number;
|
|
14
|
+
limit: number;
|
|
15
|
+
sort: string;
|
|
16
|
+
listRequest: Promise<BaseObject[]>;
|
|
17
|
+
/** `null` when the caller passed `{ insight: false }` — the aggregate query is then never made. */
|
|
18
|
+
insightRequest: Promise<BaseInsight | null>;
|
|
19
|
+
light: new (obj?: unknown) => BaseObject;
|
|
20
|
+
insight: new (obj?: unknown) => object;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The two calls a slice's `init` makes and the three values callers read off them.
|
|
25
|
+
*
|
|
26
|
+
* The list does not wait for the aggregate: `x<Slice>List` resolves as soon as the rows land, while
|
|
27
|
+
* `x<Slice>Init` — which carries `lastPageOf<Model>` and therefore needs the count — waits for both. Every
|
|
28
|
+
* derived value is memoized, so the awaited shape and the un-awaited handle hand out the same instances.
|
|
29
|
+
*/
|
|
30
|
+
export class SliceInitHandle {
|
|
31
|
+
readonly #input: SliceInitInput;
|
|
32
|
+
#serverInit?: object;
|
|
33
|
+
#modelList?: DataList<BaseObject>;
|
|
34
|
+
#modelInsight?: object;
|
|
35
|
+
|
|
36
|
+
constructor(input: SliceInitInput) {
|
|
37
|
+
this.#input = input;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
build() {
|
|
41
|
+
const { refName, capSuffix, listRequest, insightRequest } = this.#input;
|
|
42
|
+
const keys = {
|
|
43
|
+
init: `${refName}Init${capSuffix}`,
|
|
44
|
+
list: `${refName}List${capSuffix}`,
|
|
45
|
+
insight: `${refName}Insight${capSuffix}`,
|
|
46
|
+
};
|
|
47
|
+
return FetchHandle.of<Record<string, unknown>, Record<string, Promise<unknown>>>(
|
|
48
|
+
[listRequest, insightRequest],
|
|
49
|
+
async () => {
|
|
50
|
+
const [modelObjList, modelObjInsight] = await Promise.all([listRequest, insightRequest]);
|
|
51
|
+
return FetchHandle.lazy({ [keys.init]: this.#serverInitOf(modelObjList, modelObjInsight) }, {
|
|
52
|
+
[keys.list]: () => this.#listOf(modelObjList),
|
|
53
|
+
[keys.insight]: () => this.#insightOf(modelObjInsight),
|
|
54
|
+
} as { [key: string]: () => unknown });
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
[keys.init]: () =>
|
|
58
|
+
Promise.all([listRequest, insightRequest]).then(([modelObjList, modelObjInsight]) =>
|
|
59
|
+
this.#serverInitOf(modelObjList, modelObjInsight),
|
|
60
|
+
),
|
|
61
|
+
[keys.list]: () => listRequest.then((modelObjList) => this.#listOf(modelObjList)),
|
|
62
|
+
[keys.insight]: () => insightRequest.then((modelObjInsight) => this.#insightOf(modelObjInsight)),
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
#serverInitOf(modelObjList: BaseObject[], modelObjInsight: BaseInsight | null) {
|
|
68
|
+
const { refName, capRefName, sliceName, argLength, queryArgs, page, limit, sort } = this.#input;
|
|
69
|
+
const lastPage = modelObjInsight?.count
|
|
70
|
+
? Math.max(Math.floor((modelObjInsight.count - 1) / (limit || 20)) + 1, 1)
|
|
71
|
+
: 1;
|
|
72
|
+
this.#serverInit ??= {
|
|
73
|
+
refName,
|
|
74
|
+
sliceName,
|
|
75
|
+
argLength,
|
|
76
|
+
[`${refName}ObjList`]: modelObjList,
|
|
77
|
+
[`${refName}ObjInsight`]: modelObjInsight,
|
|
78
|
+
[`pageOf${capRefName}`]: page,
|
|
79
|
+
[`lastPageOf${capRefName}`]: lastPage,
|
|
80
|
+
[`limitOf${capRefName}`]: limit,
|
|
81
|
+
[`queryArgsOf${capRefName}`]: queryArgs,
|
|
82
|
+
[`sortOf${capRefName}`]: sort,
|
|
83
|
+
[`${refName}InitAt`]: new Date(),
|
|
84
|
+
};
|
|
85
|
+
return this.#serverInit;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
#listOf(modelObjList: BaseObject[]) {
|
|
89
|
+
const { light } = this.#input;
|
|
90
|
+
this.#modelList ??= new DataList(
|
|
91
|
+
withSharedInstances(() => modelObjList.map((modelObj) => new light(modelObj) as BaseObject)),
|
|
92
|
+
);
|
|
93
|
+
return this.#modelList;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#insightOf(modelObjInsight: BaseInsight | null) {
|
|
97
|
+
const { insight } = this.#input;
|
|
98
|
+
this.#modelInsight ??= new insight(modelObjInsight);
|
|
99
|
+
return this.#modelInsight;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -35,7 +35,8 @@ type ServerInitShape<
|
|
|
35
35
|
> = SliceMeta & {
|
|
36
36
|
[K in `${RefName}ObjList`]: LightObj[];
|
|
37
37
|
} & {
|
|
38
|
-
|
|
38
|
+
/** `null` when the caller passed `{ insight: false }`: the aggregate query was never made. */
|
|
39
|
+
[K in `${RefName}ObjInsight`]: InsightObj | null;
|
|
39
40
|
} & {
|
|
40
41
|
[K in `pageOf${CapRefName}`]: number;
|
|
41
42
|
} & {
|
|
@@ -100,6 +101,31 @@ export type EditReturn<RefName extends string, Full> = {
|
|
|
100
101
|
[K in `${RefName}Edit`]: ServerEdit<RefName, Full>;
|
|
101
102
|
};
|
|
102
103
|
|
|
104
|
+
/**
|
|
105
|
+
* What `fetch.view<Model>` / `fetch.edit<Model>` / `fetch.init<Model><Suffix>` return: awaitable for the shape
|
|
106
|
+
* the helper has always given, and destructurable into one promise per field for a route that wants each
|
|
107
|
+
* section to render as its own data lands instead of holding the page for the slowest.
|
|
108
|
+
*/
|
|
109
|
+
export type FetchHandleOf<Awaited, Fields> = PromiseLike<Awaited> & Fields;
|
|
110
|
+
|
|
111
|
+
export type ViewHandle<RefName extends string, Full> = FetchHandleOf<
|
|
112
|
+
ViewReturn<RefName, Full>,
|
|
113
|
+
{
|
|
114
|
+
[K in RefName]: Promise<Full>;
|
|
115
|
+
} & {
|
|
116
|
+
[K in `${RefName}View`]: Promise<ServerView<RefName, Full>>;
|
|
117
|
+
}
|
|
118
|
+
>;
|
|
119
|
+
|
|
120
|
+
export type EditHandle<RefName extends string, Full> = FetchHandleOf<
|
|
121
|
+
EditReturn<RefName, Full>,
|
|
122
|
+
{
|
|
123
|
+
[K in RefName]: Promise<Full>;
|
|
124
|
+
} & {
|
|
125
|
+
[K in `${RefName}Edit`]: Promise<ServerEdit<RefName, Full>>;
|
|
126
|
+
}
|
|
127
|
+
>;
|
|
128
|
+
|
|
103
129
|
type InitReturnShape<
|
|
104
130
|
RefName extends string,
|
|
105
131
|
CapSuffix extends string,
|
|
@@ -127,3 +153,40 @@ export type InitReturn<
|
|
|
127
153
|
Insight
|
|
128
154
|
>;
|
|
129
155
|
|
|
156
|
+
/**
|
|
157
|
+
* The list resolves without waiting for the aggregate, so `x<Slice>List` lands before `x<Slice>Init` — which
|
|
158
|
+
* carries `lastPageOf<Model>` and therefore needs the count.
|
|
159
|
+
*
|
|
160
|
+
* `x<Slice>List` and `x<Slice>Insight` hold hydrated model instances, which React Flight refuses as props to a
|
|
161
|
+
* client component: consume them in a server component and hand `x<Slice>Init` to a `Zone`.
|
|
162
|
+
*/
|
|
163
|
+
type InitHandleShape<
|
|
164
|
+
RefName extends string,
|
|
165
|
+
CapSuffix extends string,
|
|
166
|
+
Init,
|
|
167
|
+
ListItem extends { id: string },
|
|
168
|
+
Insight,
|
|
169
|
+
> = {
|
|
170
|
+
[K in `${RefName}Init${CapSuffix}`]: Promise<Init>;
|
|
171
|
+
} & {
|
|
172
|
+
[K in `${RefName}List${CapSuffix}`]: Promise<DataList<ListItem>>;
|
|
173
|
+
} & { [K in `${RefName}Insight${CapSuffix}`]: Promise<Insight> };
|
|
174
|
+
|
|
175
|
+
export type InitHandle<
|
|
176
|
+
RefName extends string,
|
|
177
|
+
Suffix extends string,
|
|
178
|
+
Light,
|
|
179
|
+
Insight,
|
|
180
|
+
Args,
|
|
181
|
+
Filter extends FilterInstance,
|
|
182
|
+
> = FetchHandleOf<
|
|
183
|
+
InitReturn<RefName, Suffix, Light, Insight, Args, Filter>,
|
|
184
|
+
InitHandleShape<
|
|
185
|
+
RefName,
|
|
186
|
+
Capitalize<Suffix>,
|
|
187
|
+
ServerInit<RefName, Light, Insight, Args, Filter>,
|
|
188
|
+
Light extends { id: string } ? Light : { id: string },
|
|
189
|
+
Insight
|
|
190
|
+
>
|
|
191
|
+
>;
|
|
192
|
+
|
|
@@ -15,7 +15,7 @@ import type {
|
|
|
15
15
|
SliceCls,
|
|
16
16
|
SliceInfoArgs,
|
|
17
17
|
} from "akanjs/signal";
|
|
18
|
-
import type {
|
|
18
|
+
import type { EditHandle, InitHandle, ServerEdit, ServerInit, ServerView, ViewHandle } from "./appliedReturn.type";
|
|
19
19
|
|
|
20
20
|
type _SliceMap<S extends SliceCls> = S[typeof SLICE_META];
|
|
21
21
|
type _RefName<S extends SliceCls> = SlceCnstRefName<S>;
|
|
@@ -47,7 +47,7 @@ type SliceInsightFetch<S extends SliceCls> = {
|
|
|
47
47
|
) => Promise<_Insight<S>>;
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
-
type SliceInit<S extends SliceCls, Suffix extends keyof _SliceMap<S>> =
|
|
50
|
+
type SliceInit<S extends SliceCls, Suffix extends keyof _SliceMap<S>> = InitHandle<
|
|
51
51
|
_RefName<S>,
|
|
52
52
|
Suffix & string,
|
|
53
53
|
_Light<S>,
|
|
@@ -67,7 +67,7 @@ type SliceServerInit<S extends SliceCls, Suffix extends keyof _SliceMap<S>> = Se
|
|
|
67
67
|
type SliceInitFetch<S extends SliceCls> = {
|
|
68
68
|
[Suffix in keyof _SliceMap<S> as Suffix extends string ? `init${_Cap<S>}${Capitalize<Suffix>}` : never]: (
|
|
69
69
|
...args: [...SliceInfoArgs<_SliceMap<S>[Suffix]>, option?: SliceInitOption<S>]
|
|
70
|
-
) =>
|
|
70
|
+
) => SliceInit<S, Suffix>;
|
|
71
71
|
};
|
|
72
72
|
|
|
73
73
|
type SliceGetInitFetch<S extends SliceCls> = {
|
|
@@ -130,11 +130,11 @@ type AppliedBaseSliceFetchType<
|
|
|
130
130
|
_CapitalizedRefName extends string = Capitalize<RefName>,
|
|
131
131
|
_PurifiedInput = PurifiedModel<Input>,
|
|
132
132
|
> = {
|
|
133
|
-
[K in `view${_CapitalizedRefName}`]: (id: string, option?: FetchPolicy) =>
|
|
133
|
+
[K in `view${_CapitalizedRefName}`]: (id: string, option?: FetchPolicy) => ViewHandle<RefName, Full>;
|
|
134
134
|
} & {
|
|
135
135
|
[K in `get${_CapitalizedRefName}View`]: (id: string, option?: FetchPolicy) => Promise<ServerView<RefName, Full>>;
|
|
136
136
|
} & {
|
|
137
|
-
[K in `edit${_CapitalizedRefName}`]: (id: string, option?: FetchPolicy) =>
|
|
137
|
+
[K in `edit${_CapitalizedRefName}`]: (id: string, option?: FetchPolicy) => EditHandle<RefName, Full>;
|
|
138
138
|
} & {
|
|
139
139
|
[K in `get${_CapitalizedRefName}Edit`]: (id: string, option?: FetchPolicy) => Promise<ServerEdit<RefName, Full>>;
|
|
140
140
|
} & {
|
package/fetch/requestStorage.ts
CHANGED
|
@@ -169,15 +169,29 @@ export function getRequestDynamicUsage(): AkanDynamicUsage | undefined {
|
|
|
169
169
|
return getRequestStore()?.dynamicUsage;
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
/**
|
|
173
|
-
|
|
172
|
+
/**
|
|
173
|
+
* Deduplicates a promise-producing query within the active request, and says whether this caller is the one
|
|
174
|
+
* that started it.
|
|
175
|
+
*
|
|
176
|
+
* `owned` is what lets the caller skip copying the response. The copy exists because one response object is
|
|
177
|
+
* handed to every caller in the request and a parsed model can hold references into it, so a second caller must
|
|
178
|
+
* not be given the same graph — but a query asked for once (which is nearly all of them: the key carries the
|
|
179
|
+
* URL and the auth headers) has no second caller to protect, and copying for it is a whole JSON round-trip
|
|
180
|
+
* spent on nothing.
|
|
181
|
+
*/
|
|
182
|
+
export function claimRequestQuery<T>(key: string, factory: () => Promise<T>): { value: Promise<T>; owned: boolean } {
|
|
174
183
|
const store = getRequestStore();
|
|
175
|
-
if (!store) return factory();
|
|
184
|
+
if (!store) return { value: factory(), owned: true };
|
|
176
185
|
const existing = store.queryCache.get(key);
|
|
177
|
-
if (existing) return existing as Promise<T
|
|
186
|
+
if (existing) return { value: existing as Promise<T>, owned: false };
|
|
178
187
|
const promise = factory();
|
|
179
188
|
store.queryCache.set(key, promise);
|
|
180
|
-
return promise;
|
|
189
|
+
return { value: promise, owned: true };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Deduplicates a promise-producing query within the active request. */
|
|
193
|
+
export function memoizeRequestQuery<T>(key: string, factory: () => Promise<T>): Promise<T> {
|
|
194
|
+
return claimRequestQuery(key, factory).value;
|
|
181
195
|
}
|
|
182
196
|
|
|
183
197
|
/** Returns current request headers as a Map, or an empty Map outside a request. */
|
package/index.ts
CHANGED
|
@@ -291,6 +291,20 @@ export interface FileConventionScanResult {
|
|
|
291
291
|
zone: { databases: string[]; services: string[]; scalars: string[] };
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
/**
|
|
295
|
+
* What a *live* scan exposes on `AppInfo.file` / `LibInfo.file`: a set per module kind, with every file
|
|
296
|
+
* type carrying all four kinds. `FileConventionScanResult` above is the serialized form that crosses a
|
|
297
|
+
* process boundary as JSON, which is why it is arrays and why each file type lists only the kinds it can
|
|
298
|
+
* hold. The two are not interchangeable — every reader of `.file` calls `.has()`.
|
|
299
|
+
*/
|
|
300
|
+
export interface FileConventionScanSet {
|
|
301
|
+
all: Set<string>;
|
|
302
|
+
databases: Set<string>;
|
|
303
|
+
services: Set<string>;
|
|
304
|
+
scalars: Set<string>;
|
|
305
|
+
}
|
|
306
|
+
export type FileConventionScanSets = { [key in keyof FileConventionScanResult]: FileConventionScanSet };
|
|
307
|
+
|
|
294
308
|
export interface ScanResult {
|
|
295
309
|
name: string;
|
|
296
310
|
type: "app" | "lib";
|
|
@@ -333,7 +347,7 @@ export interface AppInfo {
|
|
|
333
347
|
readonly database: Map<string, Set<string>>;
|
|
334
348
|
readonly service: Map<string, Set<string>>;
|
|
335
349
|
readonly scalar: Map<string, Set<string>>;
|
|
336
|
-
readonly file:
|
|
350
|
+
readonly file: FileConventionScanSets;
|
|
337
351
|
getLibs(): string[];
|
|
338
352
|
getLibInfos(): Map<string, LibInfo>;
|
|
339
353
|
getDatabaseModules(): string[];
|
|
@@ -347,7 +361,7 @@ export interface LibInfo {
|
|
|
347
361
|
readonly database: Map<string, Set<string>>;
|
|
348
362
|
readonly service: Map<string, Set<string>>;
|
|
349
363
|
readonly scalar: Map<string, Set<string>>;
|
|
350
|
-
readonly file:
|
|
364
|
+
readonly file: FileConventionScanSets;
|
|
351
365
|
getLibs(): string[];
|
|
352
366
|
getLibInfos(): Map<string, LibInfo>;
|
|
353
367
|
getLibInfo(libName: string): LibInfo | undefined;
|