@fougere/core 0.12.0-alpha.0 → 0.12.2-alpha.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/dist/CallIdentity.d.ts.map +1 -1
- package/dist/CallIdentity.js.map +1 -1
- package/dist/DomainCluster.d.ts.map +1 -1
- package/dist/DomainCluster.js +1 -1
- package/dist/DomainCluster.js.map +1 -1
- package/dist/EffectiveOperationModel.d.ts.map +1 -1
- package/dist/EffectiveOperationModel.js +20 -1
- package/dist/EffectiveOperationModel.js.map +1 -1
- package/dist/FougereConfig.d.ts.map +1 -1
- package/dist/FougereConfig.js.map +1 -1
- package/dist/FrondConfig.d.ts.map +1 -1
- package/dist/FrondConfig.js.map +1 -1
- package/dist/Source.js +2 -2
- package/dist/Source.js.map +1 -1
- package/dist/boot/AppLifecycle.d.ts.map +1 -1
- package/dist/boot/AppLifecycle.js.map +1 -1
- package/dist/boot/Emissions.d.ts.map +1 -1
- package/dist/boot/Emissions.js.map +1 -1
- package/dist/boot/ambient.als.d.ts.map +1 -1
- package/dist/boot/ambient.als.js.map +1 -1
- package/dist/boot/apply.d.ts.map +1 -1
- package/dist/boot/apply.js.map +1 -1
- package/dist/boot/arity.d.ts +13 -0
- package/dist/boot/arity.d.ts.map +1 -0
- package/dist/boot/arity.js +33 -0
- package/dist/boot/arity.js.map +1 -0
- package/dist/boot/bootstrap.d.ts.map +1 -1
- package/dist/boot/bootstrap.js +3 -2
- package/dist/boot/bootstrap.js.map +1 -1
- package/dist/boot/card.d.ts.map +1 -1
- package/dist/boot/card.js.map +1 -1
- package/dist/boot/frame.d.ts.map +1 -1
- package/dist/boot/frame.js.map +1 -1
- package/dist/boot/install.d.ts.map +1 -1
- package/dist/boot/install.js +8 -2
- package/dist/boot/install.js.map +1 -1
- package/dist/boot/ownership.d.ts.map +1 -1
- package/dist/boot/ownership.js.map +1 -1
- package/dist/boot/relations.d.ts +10 -0
- package/dist/boot/relations.d.ts.map +1 -1
- package/dist/boot/relations.js +35 -2
- package/dist/boot/relations.js.map +1 -1
- package/dist/boot/remote.d.ts.map +1 -1
- package/dist/boot/remote.js +8 -2
- package/dist/boot/remote.js.map +1 -1
- package/dist/boot/seed.js +1 -1
- package/dist/boot/seed.js.map +1 -1
- package/dist/boot/statement-drift.d.ts +6 -0
- package/dist/boot/statement-drift.d.ts.map +1 -1
- package/dist/boot/statement-drift.js +19 -0
- package/dist/boot/statement-drift.js.map +1 -1
- package/dist/boot/together.d.ts.map +1 -1
- package/dist/boot/together.js.map +1 -1
- package/dist/builtin/Carry.d.ts.map +1 -1
- package/dist/builtin/Carry.js.map +1 -1
- package/dist/builtin/Logger.d.ts.map +1 -1
- package/dist/builtin/Logger.js +3 -1
- package/dist/builtin/Logger.js.map +1 -1
- package/dist/builtin/config.js.map +1 -1
- package/dist/contract.d.ts +2 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +2 -0
- package/dist/contract.js.map +1 -1
- package/dist/crypto/encoding.d.ts.map +1 -1
- package/dist/crypto/encoding.js.map +1 -1
- package/dist/crypto/node.d.ts.map +1 -1
- package/dist/crypto/node.js.map +1 -1
- package/dist/crypto/webcrypto.d.ts.map +1 -1
- package/dist/crypto/webcrypto.js.map +1 -1
- package/dist/descriptor/Fronds.d.ts.map +1 -1
- package/dist/descriptor/Fronds.js.map +1 -1
- package/dist/dispatch/ArrayResult.d.ts.map +1 -1
- package/dist/dispatch/ArrayResult.js.map +1 -1
- package/dist/dispatch/Dispatcher.d.ts.map +1 -1
- package/dist/dispatch/Dispatcher.js.map +1 -1
- package/dist/dispatch/HandlerFacade.d.ts +6 -1
- package/dist/dispatch/HandlerFacade.d.ts.map +1 -1
- package/dist/dispatch/HandlerFacade.js +11 -3
- package/dist/dispatch/HandlerFacade.js.map +1 -1
- package/dist/dispatch/InFlight.d.ts.map +1 -1
- package/dist/dispatch/InFlight.js.map +1 -1
- package/dist/dispatch/LocalRoutePolicy.d.ts.map +1 -1
- package/dist/dispatch/LocalRoutePolicy.js.map +1 -1
- package/dist/dispatch/OutputView.d.ts.map +1 -1
- package/dist/dispatch/OutputView.js +7 -0
- package/dist/dispatch/OutputView.js.map +1 -1
- package/dist/dispatch/PresenterExecutor.d.ts.map +1 -1
- package/dist/dispatch/PresenterExecutor.js +7 -0
- package/dist/dispatch/PresenterExecutor.js.map +1 -1
- package/dist/dispatch/Received.d.ts +3 -0
- package/dist/dispatch/Received.d.ts.map +1 -0
- package/dist/dispatch/Received.js +2 -0
- package/dist/dispatch/Received.js.map +1 -0
- package/dist/dispatch/RouteRegistry.d.ts.map +1 -1
- package/dist/dispatch/RouteRegistry.js.map +1 -1
- package/dist/dispatch/StorageGuard.d.ts.map +1 -1
- package/dist/dispatch/StorageGuard.js +4 -1
- package/dist/dispatch/StorageGuard.js.map +1 -1
- package/dist/dispatch/decoded.d.ts +19 -0
- package/dist/dispatch/decoded.d.ts.map +1 -0
- package/dist/dispatch/decoded.js +36 -0
- package/dist/dispatch/decoded.js.map +1 -0
- package/dist/entry/facade.d.ts +9 -2
- package/dist/entry/facade.d.ts.map +1 -1
- package/dist/entry/facade.js +16 -7
- package/dist/entry/facade.js.map +1 -1
- package/dist/identity-keys.d.ts.map +1 -1
- package/dist/identity-keys.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js.map +1 -1
- package/dist/prefab/CrudConstructor.d.ts.map +1 -1
- package/dist/prefab/CrudConstructor.js +3 -2
- package/dist/prefab/CrudConstructor.js.map +1 -1
- package/dist/prefab/CrudOps.d.ts +2 -2
- package/dist/prefab/CrudOps.d.ts.map +1 -1
- package/dist/prefab/MirrorConstructor.js +1 -1
- package/dist/prefab/MirrorConstructor.js.map +1 -1
- package/dist/prefab/Refreshed.d.ts +7 -2
- package/dist/prefab/Refreshed.d.ts.map +1 -1
- package/dist/prefab/collector.d.ts.map +1 -1
- package/dist/prefab/collector.js.map +1 -1
- package/dist/prefab/presenter.d.ts.map +1 -1
- package/dist/prefab/presenter.js.map +1 -1
- package/dist/storage/Storage.d.ts.map +1 -1
- package/dist/storage/Storage.js.map +1 -1
- package/dist/storage/Store.d.ts.map +1 -1
- package/dist/storage/Store.js +7 -11
- package/dist/storage/Store.js.map +1 -1
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js.map +1 -1
- package/dist/wire/AppMiddleware.d.ts +10 -1
- package/dist/wire/AppMiddleware.d.ts.map +1 -1
- package/dist/wire/AppMiddleware.js.map +1 -1
- package/dist/wire/AppNext.d.ts +2 -1
- package/dist/wire/AppNext.d.ts.map +1 -1
- package/dist/wire/Facade.d.ts +14 -2
- package/dist/wire/Facade.d.ts.map +1 -1
- package/dist/wire/Facade.js.map +1 -1
- package/dist/wire/FougereError.d.ts.map +1 -1
- package/dist/wire/FougereError.js +1 -1
- package/dist/wire/FougereError.js.map +1 -1
- package/dist/wire/Invocation.d.ts.map +1 -1
- package/dist/wire/Invocation.js.map +1 -1
- package/dist/wire/OperationContract.d.ts.map +1 -1
- package/dist/wire/OperationContract.js +1 -1
- package/dist/wire/OperationContract.js.map +1 -1
- package/dist/wire/Page.d.ts +32 -0
- package/dist/wire/Page.d.ts.map +1 -0
- package/dist/wire/Page.js +32 -0
- package/dist/wire/Page.js.map +1 -0
- package/dist/wire/RouteAddress.d.ts.map +1 -1
- package/dist/wire/RouteAddress.js.map +1 -1
- package/dist/wire/Signature.d.ts +7 -0
- package/dist/wire/Signature.d.ts.map +1 -1
- package/dist/wire/binding.d.ts.map +1 -1
- package/dist/wire/binding.js.map +1 -1
- package/dist/wire/card/IdentityCard.d.ts.map +1 -1
- package/dist/wire/card/IdentityCard.js.map +1 -1
- package/dist/wire/drift.d.ts.map +1 -1
- package/dist/wire/drift.js.map +1 -1
- package/dist/wire/http-error.d.ts.map +1 -1
- package/dist/wire/http-error.js.map +1 -1
- package/dist/wire/loggerMiddleware.d.ts.map +1 -1
- package/dist/wire/loggerMiddleware.js.map +1 -1
- package/package.json +5 -5
- package/src/CallIdentity.ts +6 -0
- package/src/DomainCluster.ts +2 -1
- package/src/EffectiveOperationModel.ts +38 -1
- package/src/FougereConfig.ts +3 -0
- package/src/FrondConfig.ts +2 -0
- package/src/Source.ts +2 -2
- package/src/boot/AppLifecycle.ts +1 -0
- package/src/boot/Emissions.ts +4 -0
- package/src/boot/ambient.als.ts +2 -0
- package/src/boot/apply.ts +1 -0
- package/src/boot/arity.ts +42 -0
- package/src/boot/bootstrap.ts +9 -2
- package/src/boot/card.ts +2 -0
- package/src/boot/frame.ts +6 -0
- package/src/boot/install.ts +8 -1
- package/src/boot/ownership.ts +1 -0
- package/src/boot/relations.ts +39 -2
- package/src/boot/remote.ts +10 -2
- package/src/boot/seed.ts +1 -1
- package/src/boot/statement-drift.ts +25 -0
- package/src/boot/together.ts +3 -0
- package/src/builtin/Carry.ts +1 -0
- package/src/builtin/Logger.ts +9 -1
- package/src/builtin/config.ts +1 -0
- package/src/contract.ts +3 -0
- package/src/crypto/encoding.ts +4 -0
- package/src/crypto/node.ts +2 -0
- package/src/crypto/webcrypto.ts +2 -0
- package/src/descriptor/Fronds.ts +2 -0
- package/src/dispatch/ArrayResult.ts +1 -0
- package/src/dispatch/Dispatcher.ts +1 -0
- package/src/dispatch/HandlerFacade.ts +16 -4
- package/src/dispatch/InFlight.ts +2 -0
- package/src/dispatch/LocalRoutePolicy.ts +1 -0
- package/src/dispatch/OutputView.ts +7 -0
- package/src/dispatch/PresenterExecutor.ts +7 -0
- package/src/dispatch/Received.ts +2 -0
- package/src/dispatch/RouteRegistry.ts +4 -0
- package/src/dispatch/StorageGuard.ts +9 -1
- package/src/dispatch/decoded.ts +37 -0
- package/src/entry/facade.ts +21 -9
- package/src/identity-keys.ts +2 -0
- package/src/index.ts +1 -0
- package/src/loader.ts +1 -0
- package/src/prefab/CrudConstructor.ts +4 -2
- package/src/prefab/CrudOps.ts +2 -2
- package/src/prefab/MirrorConstructor.ts +1 -1
- package/src/prefab/Refreshed.ts +7 -2
- package/src/prefab/collector.ts +1 -0
- package/src/prefab/presenter.ts +1 -0
- package/src/storage/Storage.ts +2 -0
- package/src/storage/Store.ts +15 -10
- package/src/verify.ts +2 -0
- package/src/wire/AppMiddleware.ts +12 -1
- package/src/wire/AppNext.ts +2 -1
- package/src/wire/Facade.ts +14 -2
- package/src/wire/FougereError.ts +3 -1
- package/src/wire/Invocation.ts +2 -0
- package/src/wire/OperationContract.ts +2 -1
- package/src/wire/Page.ts +50 -0
- package/src/wire/RouteAddress.ts +2 -0
- package/src/wire/Signature.ts +7 -0
- package/src/wire/binding.ts +1 -0
- package/src/wire/card/IdentityCard.ts +1 -0
- package/src/wire/drift.ts +3 -0
- package/src/wire/http-error.ts +2 -0
- package/src/wire/loggerMiddleware.ts +1 -0
package/src/builtin/Carry.ts
CHANGED
package/src/builtin/Logger.ts
CHANGED
|
@@ -12,6 +12,7 @@ let threshold: number = LEVELS[envLevel() ?? 'info'];
|
|
|
12
12
|
/** The level the PROCESS was started with. */
|
|
13
13
|
export function envLevel(): LogLevel | undefined {
|
|
14
14
|
const raw = typeof process === 'undefined' ? undefined : process.env.FOUGERE_LOG_LEVEL;
|
|
15
|
+
|
|
15
16
|
return raw !== undefined && raw in LEVELS ? (raw as LogLevel) : undefined;
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -51,8 +52,10 @@ function supportsColor(): boolean {
|
|
|
51
52
|
if (typeof process !== 'undefined') {
|
|
52
53
|
if (process.env.NO_COLOR) return false;
|
|
53
54
|
if (process.env.FORCE_COLOR === '1') return true;
|
|
55
|
+
|
|
54
56
|
return process.stdout?.isTTY === true;
|
|
55
57
|
}
|
|
58
|
+
|
|
56
59
|
return false;
|
|
57
60
|
}
|
|
58
61
|
|
|
@@ -62,6 +65,7 @@ function stamp(at: number | Date): string {
|
|
|
62
65
|
const m = String(d.getMinutes()).padStart(2, '0');
|
|
63
66
|
const s = String(d.getSeconds()).padStart(2, '0');
|
|
64
67
|
const ms = String(d.getMilliseconds()).padStart(3, '0');
|
|
68
|
+
|
|
65
69
|
return `${h}:${m}:${s}.${ms}`;
|
|
66
70
|
}
|
|
67
71
|
|
|
@@ -92,7 +96,7 @@ export class Logger {
|
|
|
92
96
|
level: level as LogRecord['level'], name: this.name, message: msg, args, at: Date.now(),
|
|
93
97
|
};
|
|
94
98
|
|
|
95
|
-
this.carry?.push(record);
|
|
99
|
+
this.carry?.push({ ...record, args: args.map(objectOf) });
|
|
96
100
|
|
|
97
101
|
// The console ALWAYS, whoever else took the line. Skipping it once a destination
|
|
98
102
|
// existed made `calls()` — a devtools ring that prints nothing — silence the
|
|
@@ -103,6 +107,10 @@ export class Logger {
|
|
|
103
107
|
}
|
|
104
108
|
}
|
|
105
109
|
|
|
110
|
+
/** A line travels as a fact, where `args` states `json()` — an object. */
|
|
111
|
+
const objectOf = (arg: unknown): object =>
|
|
112
|
+
typeof arg === 'object' && arg !== null && !Array.isArray(arg) ? arg : { value: arg };
|
|
113
|
+
|
|
106
114
|
export function formatted(
|
|
107
115
|
record: Rendered,
|
|
108
116
|
color = supportsColor(),
|
package/src/builtin/config.ts
CHANGED
package/src/contract.ts
CHANGED
|
@@ -29,6 +29,7 @@ export { MAX_BODY_BYTES } from './wire/SignedCall.js';
|
|
|
29
29
|
// call log ignoring its own reader — has to be able to name it.
|
|
30
30
|
export { RPC_ENTITY } from './wire/RpcAnswer.js';
|
|
31
31
|
export type { CallPage } from './wire/CallPage.js';
|
|
32
|
+
export { pageOf, asPage, type Page } from './wire/Page.js';
|
|
32
33
|
export type { CallRecord } from './wire/CallRecord.js';
|
|
33
34
|
// The comparison of two cards, which a consumer runs about a producer — browser-safe on
|
|
34
35
|
// purpose: a panel showing the drift holds only the two cards, never the app.
|
|
@@ -55,6 +56,7 @@ export type { Edge } from './wire/topology/Edge.js';
|
|
|
55
56
|
export type { FrondPlacement } from './wire/topology/FrondPlacement.js';
|
|
56
57
|
export type { TopologyReport } from './wire/topology/TopologyReport.js';
|
|
57
58
|
export { assertIdentityCard } from './wire/card/IdentityCard.js';
|
|
59
|
+
export { decoded } from './dispatch/decoded.js';
|
|
58
60
|
|
|
59
61
|
/** The key a class name is filed under — 'Post' → 'post'. */
|
|
60
62
|
export { lowerFirst } from '@fougere/schema';
|
|
@@ -78,5 +80,6 @@ export function callValueOf(
|
|
|
78
80
|
typeof opOrInput === 'string'
|
|
79
81
|
? [{ entity: lowerFirst((target as { name: string }).name), op: opOrInput }, input]
|
|
80
82
|
: [target as FrondCall, opOrInput];
|
|
83
|
+
|
|
81
84
|
return { call, invocation: Invocation.from(given) };
|
|
82
85
|
}
|
package/src/crypto/encoding.ts
CHANGED
|
@@ -13,6 +13,7 @@ export function b64url(input: Uint8Array | string): string {
|
|
|
13
13
|
// One character at a time: `String.fromCharCode(...bytes)` overflows the argument
|
|
14
14
|
// limit on a key, and a signature is small enough that the loop costs nothing.
|
|
15
15
|
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
16
|
+
|
|
16
17
|
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
17
18
|
}
|
|
18
19
|
|
|
@@ -20,6 +21,7 @@ export function unb64url(input: string): Uint8Array {
|
|
|
20
21
|
const binary = atob(input.replace(/-/g, '+').replace(/_/g, '/'));
|
|
21
22
|
const bytes = new Uint8Array(binary.length);
|
|
22
23
|
for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
|
|
24
|
+
|
|
23
25
|
return bytes;
|
|
24
26
|
}
|
|
25
27
|
|
|
@@ -28,6 +30,7 @@ export const unb64 = (input: string): Uint8Array => {
|
|
|
28
30
|
const binary = atob(input);
|
|
29
31
|
const bytes = new Uint8Array(binary.length);
|
|
30
32
|
for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
|
|
33
|
+
|
|
31
34
|
return bytes;
|
|
32
35
|
};
|
|
33
36
|
|
|
@@ -35,5 +38,6 @@ export const unb64 = (input: string): Uint8Array => {
|
|
|
35
38
|
export function derOf(pem: string): Uint8Array {
|
|
36
39
|
const body = pem.replace(/-----(BEGIN|END)[^-]+-----/g, '').replace(/\s+/g, '');
|
|
37
40
|
if (body.length === 0) throw new Error('Not a PEM: no base64 body between the armour');
|
|
41
|
+
|
|
38
42
|
return unb64(body);
|
|
39
43
|
}
|
package/src/crypto/node.ts
CHANGED
|
@@ -13,6 +13,7 @@ export const crypto: CryptoPort = {
|
|
|
13
13
|
async signerOf(privateKeyPem): Promise<Signer> {
|
|
14
14
|
// Parsed here and captured, so a call signs against a KeyObject and never a string.
|
|
15
15
|
const key = createPrivateKey(privateKeyPem);
|
|
16
|
+
|
|
16
17
|
return { async sign(data) { return new Uint8Array(sign(null, data, key)); } };
|
|
17
18
|
},
|
|
18
19
|
|
|
@@ -20,6 +21,7 @@ export const crypto: CryptoPort = {
|
|
|
20
21
|
const parsed = typeof key === 'string'
|
|
21
22
|
? createPublicKey(key)
|
|
22
23
|
: createPublicKey({ key: key as never, format: 'jwk' });
|
|
24
|
+
|
|
23
25
|
return { async verify(data, signature) { return verify(null, data, parsed, signature); } };
|
|
24
26
|
},
|
|
25
27
|
};
|
package/src/crypto/webcrypto.ts
CHANGED
|
@@ -33,6 +33,7 @@ export const crypto: CryptoPort = {
|
|
|
33
33
|
// Imported once, at boot. Doing it per call would put an async key parse inside
|
|
34
34
|
// every outgoing request — the reason the port hands back a signer at all.
|
|
35
35
|
const key = await subtle().importKey('pkcs8', derOf(privateKeyPem), ED25519, false, ['sign']);
|
|
36
|
+
|
|
36
37
|
return { async sign(data) { return new Uint8Array(await subtle().sign(ED25519, key, data)); } };
|
|
37
38
|
},
|
|
38
39
|
|
|
@@ -40,6 +41,7 @@ export const crypto: CryptoPort = {
|
|
|
40
41
|
const imported = typeof key === 'string'
|
|
41
42
|
? await subtle().importKey('spki', derOf(key), ED25519, false, ['verify'])
|
|
42
43
|
: await subtle().importKey('jwk', key, ED25519, false, ['verify']);
|
|
44
|
+
|
|
43
45
|
return { verify: (data, signature) => subtle().verify(ED25519, imported, signature, data) };
|
|
44
46
|
},
|
|
45
47
|
};
|
package/src/descriptor/Fronds.ts
CHANGED
|
@@ -12,6 +12,7 @@ export class Fronds extends Array<FrondDescriptor> {
|
|
|
12
12
|
static hosting(fronds: readonly FrondDescriptor[]): Fronds {
|
|
13
13
|
const all = new Fronds();
|
|
14
14
|
all.push(...fronds);
|
|
15
|
+
|
|
15
16
|
return all;
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -26,6 +27,7 @@ export class Fronds extends Array<FrondDescriptor> {
|
|
|
26
27
|
const found = frond.entities.find((e) => e.name === name);
|
|
27
28
|
if (found) return found;
|
|
28
29
|
}
|
|
30
|
+
|
|
29
31
|
return undefined;
|
|
30
32
|
}
|
|
31
33
|
|
|
@@ -71,6 +71,7 @@ export class Dispatcher implements DispatchPort {
|
|
|
71
71
|
this.lifecycle.publish(DispatchEvent.resolved(call, route.kind));
|
|
72
72
|
const result = await route.execute(call);
|
|
73
73
|
this.lifecycle.publish(DispatchEvent.completed(call, route.kind));
|
|
74
|
+
|
|
74
75
|
return result;
|
|
75
76
|
} catch (error) {
|
|
76
77
|
this.lifecycle.publish(DispatchEvent.failed(call, error, route?.kind));
|
|
@@ -116,15 +116,23 @@ export class HandlerFacade {
|
|
|
116
116
|
|
|
117
117
|
const args = contract.binding ? await this.arguments.resolve(contract.binding, validated) : [];
|
|
118
118
|
const { instance, method } = this.resolveImplementation(op);
|
|
119
|
-
const
|
|
120
|
-
const output = view.project(await instance[method](...args));
|
|
119
|
+
const answered = await instance[method](...args);
|
|
121
120
|
|
|
121
|
+
const view = this.viewOf(op);
|
|
122
122
|
const { presenter } = this.facade;
|
|
123
|
+
const computed = view.closed || !presenter
|
|
124
|
+
? answered
|
|
125
|
+
: await this.present(op, presenter, answered, validated);
|
|
123
126
|
|
|
124
|
-
return view.
|
|
127
|
+
return view.project(computed);
|
|
125
128
|
}
|
|
126
129
|
|
|
127
|
-
/**
|
|
130
|
+
/**
|
|
131
|
+
* The computed fields a presenter adds, over the rows the handler answered and BEFORE the
|
|
132
|
+
* view projects them. A presenter runs on the server, so what it reads is the row the
|
|
133
|
+
* storage holds: a `writeOnly` field is gone by the time the client sees it, and deriving
|
|
134
|
+
* from one is the whole reason to declare it.
|
|
135
|
+
*/
|
|
128
136
|
private present(
|
|
129
137
|
op: string,
|
|
130
138
|
presenter: PresenterEntry,
|
|
@@ -180,12 +188,14 @@ export class HandlerFacade {
|
|
|
180
188
|
operation?.outputClosed ?? false,
|
|
181
189
|
);
|
|
182
190
|
this.cachedViews.set(op, resolved);
|
|
191
|
+
|
|
183
192
|
return resolved;
|
|
184
193
|
}
|
|
185
194
|
|
|
186
195
|
/** The handler itself, resolved on first call — never at boot. */
|
|
187
196
|
private resolveHandler(): any {
|
|
188
197
|
if (!this.instance) this.instance = this.scope.resolve(this.handlerKey);
|
|
198
|
+
|
|
189
199
|
return this.instance;
|
|
190
200
|
}
|
|
191
201
|
|
|
@@ -208,6 +218,7 @@ export class HandlerFacade {
|
|
|
208
218
|
+ `${operation.implementation.method}, but boot found ${matches.length} matching handlers.`,
|
|
209
219
|
);
|
|
210
220
|
}
|
|
221
|
+
|
|
211
222
|
return matches[0]!;
|
|
212
223
|
}
|
|
213
224
|
|
|
@@ -225,6 +236,7 @@ export class HandlerFacade {
|
|
|
225
236
|
instance = this.scope.resolve(key);
|
|
226
237
|
this.implementationInstances.set(key, instance);
|
|
227
238
|
}
|
|
239
|
+
|
|
228
240
|
return { instance, method: operation.implementation.method };
|
|
229
241
|
}
|
|
230
242
|
}
|
package/src/dispatch/InFlight.ts
CHANGED
|
@@ -23,6 +23,7 @@ export class InFlight {
|
|
|
23
23
|
|
|
24
24
|
this.running++;
|
|
25
25
|
let released = false;
|
|
26
|
+
|
|
26
27
|
return () => {
|
|
27
28
|
if (released) return;
|
|
28
29
|
released = true;
|
|
@@ -41,6 +42,7 @@ export class InFlight {
|
|
|
41
42
|
|
|
42
43
|
whenIdle(): Promise<void> {
|
|
43
44
|
if (this.running === 0) return Promise.resolve();
|
|
45
|
+
|
|
44
46
|
return new Promise((resolve) => this.idle.push(resolve));
|
|
45
47
|
}
|
|
46
48
|
}
|
|
@@ -22,6 +22,7 @@ export class LocalRoutePolicy implements RoutePolicy {
|
|
|
22
22
|
const hosted = this.hostedNames(call.address.surface);
|
|
23
23
|
const entity = call.address.entity;
|
|
24
24
|
const surface = call.address.surface;
|
|
25
|
+
|
|
25
26
|
return new FougereError({
|
|
26
27
|
code: ErrorCode.NOT_FOUND,
|
|
27
28
|
message: (surface
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Visibility, type Fields } from '@fougere/schema';
|
|
2
2
|
import { preserveArrayProperties } from './ArrayResult.js';
|
|
3
|
+
import { asPage } from '../wire/Page.js';
|
|
3
4
|
|
|
4
5
|
/** What one operation result may show. One of the two exits a validator watches. */
|
|
5
6
|
export class OutputView {
|
|
@@ -15,6 +16,12 @@ export class OutputView {
|
|
|
15
16
|
return preserveArrayProperties(result, result.map((item) => this.project(item)));
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
// A paged op answers an envelope, so what a view applies to is its ROWS. Projecting the
|
|
20
|
+
// envelope itself let every field of every row through — `Visibility.encode` finds none of
|
|
21
|
+
// its own keys on `{ items, total }` and hands the object back.
|
|
22
|
+
const page = asPage(result, this.fields);
|
|
23
|
+
if (page) return { ...page, items: page.items.map((item) => this.project(item)) };
|
|
24
|
+
|
|
18
25
|
if (typeof result !== 'object') return result;
|
|
19
26
|
|
|
20
27
|
const record = result as Record<string, unknown>;
|
|
@@ -2,6 +2,7 @@ import { ErrorCode } from '../wire/ErrorCode.js';
|
|
|
2
2
|
import { FougereError } from '../wire/FougereError.js';
|
|
3
3
|
import { preserveArrayProperties } from './ArrayResult.js';
|
|
4
4
|
import type { PresenterArgs } from './PresenterArgs.js';
|
|
5
|
+
import { asPage } from '../wire/Page.js';
|
|
5
6
|
|
|
6
7
|
/** Adds a presenter's computed fields after output projection. */
|
|
7
8
|
export class PresenterExecutor {
|
|
@@ -17,6 +18,12 @@ export class PresenterExecutor {
|
|
|
17
18
|
return result;
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
// A paged op answers an envelope, and what a computed field sits on is a ROW. Handed the
|
|
22
|
+
// envelope itself, a presenter added its fields to `{ items, total }` and every row came
|
|
23
|
+
// back untouched.
|
|
24
|
+
const page = asPage(result, Object.fromEntries(this.fieldNames.map((name) => [name, true])));
|
|
25
|
+
if (page) return { ...page, items: await this.present(page.items, args) as unknown[] };
|
|
26
|
+
|
|
20
27
|
const rows = Array.isArray(result) ? result : [result];
|
|
21
28
|
const values = new Map<string, unknown[]>();
|
|
22
29
|
for (const name of this.fieldNames) {
|
|
@@ -35,6 +35,7 @@ export class RouteRegistry {
|
|
|
35
35
|
entity: address.entity,
|
|
36
36
|
operation: address.operation,
|
|
37
37
|
}).key());
|
|
38
|
+
|
|
38
39
|
return shared?.kind === 'system' ? shared : undefined;
|
|
39
40
|
}
|
|
40
41
|
|
|
@@ -48,6 +49,7 @@ export class RouteRegistry {
|
|
|
48
49
|
|
|
49
50
|
const resolution = this.resolveUnknown(address).finally(() => this.pending.delete(key));
|
|
50
51
|
this.pending.set(key, resolution);
|
|
52
|
+
|
|
51
53
|
return resolution;
|
|
52
54
|
}
|
|
53
55
|
|
|
@@ -56,8 +58,10 @@ export class RouteRegistry {
|
|
|
56
58
|
const route = await resolver(address);
|
|
57
59
|
if (!route) continue;
|
|
58
60
|
this.register(route);
|
|
61
|
+
|
|
59
62
|
return route;
|
|
60
63
|
}
|
|
64
|
+
|
|
61
65
|
return undefined;
|
|
62
66
|
}
|
|
63
67
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FieldSet, FieldValueValidator, InputRefusal, type Fields, type Verdict } from '@fougere/schema';
|
|
1
|
+
import { FieldSet, FieldValueValidator, InputRefusal, Role, type Fields, type Verdict } from '@fougere/schema';
|
|
2
2
|
import { COMPARISONS, comparisonOf, unknownIn } from '../storage/Comparison.js';
|
|
3
3
|
import { assertListOptions } from '../storage/Storage.js';
|
|
4
4
|
import { ErrorCode } from '../wire/ErrorCode.js';
|
|
@@ -69,12 +69,14 @@ export class StorageGuard {
|
|
|
69
69
|
guarded.create = async function (...args) {
|
|
70
70
|
args[0] = validation.validated(args[0], 'create');
|
|
71
71
|
await validation.targetsOf([args[0]], 'create');
|
|
72
|
+
|
|
72
73
|
return writer.create.apply(this, args);
|
|
73
74
|
};
|
|
74
75
|
|
|
75
76
|
guarded.update = async function (...args) {
|
|
76
77
|
args[1] = validation.validated(args[1], 'update');
|
|
77
78
|
await validation.targetsOf([args[1]], 'update');
|
|
79
|
+
|
|
78
80
|
return writer.update.apply(this, args);
|
|
79
81
|
};
|
|
80
82
|
|
|
@@ -83,6 +85,7 @@ export class StorageGuard {
|
|
|
83
85
|
guarded.upsert = async function (...args) {
|
|
84
86
|
args[0] = validation.validated(args[0], 'upsert');
|
|
85
87
|
await validation.targetsOf([args[0]], 'upsert');
|
|
88
|
+
|
|
86
89
|
return upsert.apply(this, args);
|
|
87
90
|
};
|
|
88
91
|
}
|
|
@@ -94,6 +97,7 @@ export class StorageGuard {
|
|
|
94
97
|
guarded.upsertAll = async function (...args) {
|
|
95
98
|
args[0] = args[0].map((row, index) => validation.validated(row, 'upsertAll', index));
|
|
96
99
|
await validation.targetsOf(args[0], 'upsertAll');
|
|
100
|
+
|
|
97
101
|
return upsertAll.apply(this, args);
|
|
98
102
|
};
|
|
99
103
|
}
|
|
@@ -122,6 +126,7 @@ export class StorageGuard {
|
|
|
122
126
|
private rowAt(row: unknown, index: number): string {
|
|
123
127
|
const primary = FieldSet.of(this.fields).primary;
|
|
124
128
|
const key = primary === undefined ? undefined : (row as Record<string, unknown>)[primary];
|
|
129
|
+
|
|
125
130
|
return key === undefined ? `row ${index} of this page` : `row ${primary} ${JSON.stringify(key)}`;
|
|
126
131
|
}
|
|
127
132
|
|
|
@@ -213,6 +218,8 @@ export class StorageGuard {
|
|
|
213
218
|
private written(key: string, item: unknown): Verdict {
|
|
214
219
|
const field = this.fields[key];
|
|
215
220
|
if (!field) return { message: InputRefusal.unknownField };
|
|
221
|
+
// Declared, and still nothing to write: the other side of the relation carries the key.
|
|
222
|
+
if (Role.of(field).isCollection()) return { message: InputRefusal.readOnly };
|
|
216
223
|
if (item === undefined) return { value: item };
|
|
217
224
|
|
|
218
225
|
return FieldValueValidator.of(field).parse(item);
|
|
@@ -220,6 +227,7 @@ export class StorageGuard {
|
|
|
220
227
|
|
|
221
228
|
private value(field: Fields[string], asked: unknown): Verdict {
|
|
222
229
|
if (asked === null || asked === undefined) return { value: asked };
|
|
230
|
+
|
|
223
231
|
return FieldValueValidator.of(field).parse(asked);
|
|
224
232
|
}
|
|
225
233
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Visibility, type Fields, type SchemaView } from '@fougere/schema';
|
|
2
|
+
import { preserveArrayProperties } from './ArrayResult.js';
|
|
3
|
+
import { asPage } from '../wire/Page.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* What a caller receives, read back into the shape its type promises — the dual of
|
|
7
|
+
* `OutputView.project`, applied on the other side of the call.
|
|
8
|
+
*
|
|
9
|
+
* `date-time` means a `Date` on both sides (`Boundary.forShape`), and only the outgoing half was
|
|
10
|
+
* ever applied: `Visibility.encode` turned `new Date(0)` into `"1970-01-01T00:00:00.000Z"` and
|
|
11
|
+
* nobody turned it back, so `Facade<PostHandler>` promised `createdAt: Date` and handed over a
|
|
12
|
+
* string — in this process as well as across a wire.
|
|
13
|
+
*
|
|
14
|
+
* Applied by the facade a caller HOLDS, never by the one that answers: a row leaves as data,
|
|
15
|
+
* which is what a Rust frond or a plain HTTP client reads, and the codecs are what this side
|
|
16
|
+
* knows how to put back. Measured 2026-09-18: 389 ns per row against the 1 277 ns encoding one
|
|
17
|
+
* already costs.
|
|
18
|
+
*
|
|
19
|
+
* Documented: [the gradient](https://fougere.dev/docs/infra/gradient).
|
|
20
|
+
*/
|
|
21
|
+
export function decoded(schema: SchemaView | undefined, answer: unknown): unknown {
|
|
22
|
+
if (!schema || answer === null || answer === undefined) return answer;
|
|
23
|
+
|
|
24
|
+
const fields = schema.getFields() as Fields;
|
|
25
|
+
const visibility = Visibility.of(fields);
|
|
26
|
+
const row = (one: unknown) => (one !== null && typeof one === 'object' && !Array.isArray(one)
|
|
27
|
+
? visibility.decode(one as Record<string, unknown>)
|
|
28
|
+
: one);
|
|
29
|
+
|
|
30
|
+
const page = asPage(answer, fields);
|
|
31
|
+
if (page) return { ...page, items: page.items.map(row) };
|
|
32
|
+
|
|
33
|
+
// An op annotating `ListResult<T>` still answers the array those properties ride on.
|
|
34
|
+
return Array.isArray(answer)
|
|
35
|
+
? preserveArrayProperties(answer, answer.map(row))
|
|
36
|
+
: row(answer);
|
|
37
|
+
}
|
package/src/entry/facade.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Call } from '../wire/Call.js';
|
|
|
2
2
|
|
|
3
3
|
import { RouteAddress } from '../wire/RouteAddress.js';
|
|
4
4
|
import type { DispatchPort } from '../dispatch/DispatchPort.js';
|
|
5
|
+
import type { Received } from '../dispatch/Received.js';
|
|
5
6
|
|
|
6
7
|
type Operation = (...args: any[]) => unknown;
|
|
7
8
|
|
|
@@ -22,21 +23,32 @@ export function dynamicOperations(operation: (name: string) => Operation): Recor
|
|
|
22
23
|
});
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Turns facade method calls into canonical dispatches.
|
|
28
|
+
*
|
|
29
|
+
* `received` is what this side puts back before handing the answer over: a row leaves as data
|
|
30
|
+
* and `date-time` means a `Date` on both sides. A facade built without one hands over what the
|
|
31
|
+
* wire carried, which is what a caller holding no schema can do.
|
|
32
|
+
*/
|
|
26
33
|
export function facadeOperations(
|
|
27
34
|
dispatcher: DispatchPort,
|
|
28
35
|
entity: string,
|
|
29
36
|
operationNames?: Iterable<string>,
|
|
30
37
|
surface?: string,
|
|
38
|
+
received?: Received,
|
|
31
39
|
): Record<string, Operation> {
|
|
32
|
-
const operation = (name: string): Operation => (invocation) =>
|
|
33
|
-
new
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
const operation = (name: string): Operation => async (invocation) => {
|
|
41
|
+
const answer = await dispatcher.dispatch(new Call(
|
|
42
|
+
new RouteAddress({
|
|
43
|
+
entity,
|
|
44
|
+
operation: name,
|
|
45
|
+
...(surface !== undefined ? { surface } : {}),
|
|
46
|
+
}),
|
|
47
|
+
invocation,
|
|
48
|
+
));
|
|
49
|
+
|
|
50
|
+
return received ? received(name, answer) : answer;
|
|
51
|
+
};
|
|
40
52
|
|
|
41
53
|
return operationNames
|
|
42
54
|
? Object.fromEntries([...operationNames].map((name) => [name, operation(name)]))
|
package/src/identity-keys.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { b64url } from './crypto/encoding.js';
|
|
|
5
5
|
/** A fresh Ed25519 pair, PEM both ways. The root's and a frond's are the same kind. */
|
|
6
6
|
export function generateKeyPair(): { privateKey: string; publicKey: string } {
|
|
7
7
|
const { privateKey, publicKey } = generateKeyPairSync('ed25519');
|
|
8
|
+
|
|
8
9
|
return {
|
|
9
10
|
privateKey: privateKey.export({ type: 'pkcs8', format: 'pem' }).toString(),
|
|
10
11
|
publicKey: publicKey.export({ type: 'spki', format: 'pem' }).toString(),
|
|
@@ -26,5 +27,6 @@ export function issueGrant(
|
|
|
26
27
|
const header = b64url(JSON.stringify({ alg: 'EdDSA', typ: 'fougere-grant' }));
|
|
27
28
|
const payload = b64url(JSON.stringify({ sub: name, jwk, iat: now, exp: now + ttlDays * 86_400_000 }));
|
|
28
29
|
const signingInput = `${header}.${payload}`;
|
|
30
|
+
|
|
29
31
|
return `${signingInput}.${b64url(new Uint8Array(sign(null, Buffer.from(signingInput), createPrivateKey(rootPrivateKey))))}`;
|
|
30
32
|
}
|
package/src/index.ts
CHANGED
|
@@ -49,6 +49,7 @@ export { JOURNAL, type Journal } from './dispatch/Journal.js';
|
|
|
49
49
|
// them — or a test of one — has to be able to make one through the facade.
|
|
50
50
|
export { DispatchEvent } from './dispatch/DispatchEvent.js';
|
|
51
51
|
export type { CallPage, CallRecord } from './contract.js';
|
|
52
|
+
export { pageOf, asPage, type Page } from './contract.js';
|
|
52
53
|
export { driftOf, agrees, explain, type CardDrift } from './contract.js';
|
|
53
54
|
export type { OperationContract } from './wire/OperationContract.js';
|
|
54
55
|
export type { OperationsMap } from './wire/OperationsMap.js';
|
package/src/loader.ts
CHANGED
|
@@ -11,6 +11,7 @@ const defaultLoader: ModuleLoader = async (filePath, options) => {
|
|
|
11
11
|
// On the URL, never on the path: `pathToFileURL` percent-encodes a `?` into the
|
|
12
12
|
// filename, and the import then looks for a file whose name ends in `%3Fv=…`.
|
|
13
13
|
const url = pathToFileURL(filePath).href;
|
|
14
|
+
|
|
14
15
|
return await import(options?.fresh ? `${url}?v=${Date.now()}` : url);
|
|
15
16
|
};
|
|
16
17
|
|
|
@@ -6,6 +6,7 @@ import type { OperationContract } from '../wire/OperationContract.js';
|
|
|
6
6
|
import { targetOf } from './prefab.js';
|
|
7
7
|
import type { CrudOpName } from './CrudOpName.js';
|
|
8
8
|
import type { CrudViews } from './CrudViews.js';
|
|
9
|
+
import { pageOf, type Page } from '../wire/Page.js';
|
|
9
10
|
import type { CrudOps } from './CrudOps.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -51,12 +52,13 @@ const returns = (raw: string, name: string, extra?: { array?: boolean; nullable?
|
|
|
51
52
|
function crudOps(entity: SchemaView & { partial?: () => SchemaView }): Record<string, OperationContract> {
|
|
52
53
|
const name = (entity as { name?: string }).name ?? 'Entity';
|
|
53
54
|
const input = inputParam(name);
|
|
55
|
+
|
|
54
56
|
return {
|
|
55
57
|
list: {
|
|
56
58
|
output: entity, cardinality: 'page',
|
|
57
59
|
binding: [{ name: 'options', source: { kind: 'query' }, optional: true }],
|
|
58
60
|
signature: {
|
|
59
|
-
name: 'list', returnType: returns(`
|
|
61
|
+
name: 'list', returnType: returns(`Page<${name}>`, 'Page'),
|
|
60
62
|
params: [{ name: 'options', type: { raw: 'ListOptions', name: 'ListOptions' }, optional: true }],
|
|
61
63
|
},
|
|
62
64
|
},
|
|
@@ -139,7 +141,7 @@ export function Crud<E extends EntityConstructor, V extends CrudViews | EntityCo
|
|
|
139
141
|
this.storage = storage as Storage<T>;
|
|
140
142
|
}
|
|
141
143
|
|
|
142
|
-
async list(options?: ListOptions): Promise<
|
|
144
|
+
async list(options?: ListOptions): Promise<Page<T>> { return pageOf(await this.storage.list(options)); }
|
|
143
145
|
async findById(id: string): Promise<T | undefined> { return this.storage.findById(id); }
|
|
144
146
|
async create(input: Partial<T>): Promise<T> { return this.storage.create(input); }
|
|
145
147
|
async update(id: string, input: Partial<T>): Promise<T> { return this.storage.update(id, input); }
|
package/src/prefab/CrudOps.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { CrudOpName } from './CrudOpName.js';
|
|
2
2
|
import type { EntityConstructor } from '@fougere/schema';
|
|
3
3
|
import type { ListOptions } from '../storage/ListOptions.js';
|
|
4
|
-
import type {
|
|
4
|
+
import type { Page } from '../wire/Page.js';
|
|
5
5
|
import type { Storage } from '../storage/Storage.js';
|
|
6
6
|
|
|
7
7
|
/** The view an op emits, fabricated. */
|
|
@@ -16,7 +16,7 @@ type OutOf<V, K extends CrudOpName, T> =
|
|
|
16
16
|
/** The five ops, typed from the entity and its views. */
|
|
17
17
|
export interface CrudOps<T, V = {}> {
|
|
18
18
|
storage: Storage<T>;
|
|
19
|
-
list(options?: ListOptions, ...collected: never[]): Promise<
|
|
19
|
+
list(options?: ListOptions, ...collected: never[]): Promise<Page<OutOf<V, 'list', T>>>;
|
|
20
20
|
findById(id: string, ...collected: never[]): Promise<OutOf<V, 'findById', T> | undefined>;
|
|
21
21
|
create(input: Partial<T>, ...collected: never[]): Promise<OutOf<V, 'create', T>>;
|
|
22
22
|
update(id: string, input: Partial<T>, ...collected: never[]): Promise<OutOf<V, 'update', T>>;
|
|
@@ -27,7 +27,7 @@ export function Mirror<E extends EntityConstructor>(shape: E): MirrorConstructor
|
|
|
27
27
|
written += await this.storage.upsertAll(page);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
return { written, since, ms: Date.now() - started };
|
|
30
|
+
return { written, ...(since && { since: since.toISOString() }), ms: Date.now() - started };
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
package/src/prefab/Refreshed.ts
CHANGED
|
@@ -2,8 +2,13 @@
|
|
|
2
2
|
export interface Refreshed {
|
|
3
3
|
/** Instances written, counting a replaced one once. */
|
|
4
4
|
written: number;
|
|
5
|
-
/**
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* The age the pull was asked to start from, as an ISO string — absent when it asked for
|
|
7
|
+
* everything. A STRING because a refresh is an operation: what it answers leaves through a
|
|
8
|
+
* facade, where only data crosses, and a `Date` reaches a caller here and an ISO string
|
|
9
|
+
* behind `fronds:` off the same code.
|
|
10
|
+
*/
|
|
11
|
+
since?: string;
|
|
7
12
|
/** How long the whole pass took, pull included. */
|
|
8
13
|
ms: number;
|
|
9
14
|
}
|
package/src/prefab/collector.ts
CHANGED
package/src/prefab/presenter.ts
CHANGED
package/src/storage/Storage.ts
CHANGED
|
@@ -159,6 +159,7 @@ const HELD = 'Storage';
|
|
|
159
159
|
*/
|
|
160
160
|
export function togetherKeyOf(entities: readonly string[], providers: readonly string[] = []): string {
|
|
161
161
|
const named = entities.map(upperFirst).join(SEPARATOR);
|
|
162
|
+
|
|
162
163
|
return `${named}${providers.length ? KINDS + providers.map(upperFirst).join(SEPARATOR) : ''}${FRAME}`;
|
|
163
164
|
}
|
|
164
165
|
|
|
@@ -174,5 +175,6 @@ export function membersOfTogetherKey(key: string): { entities: string[]; provide
|
|
|
174
175
|
if (key.length <= FRAME.length || !key.endsWith(FRAME)) return undefined;
|
|
175
176
|
const [entities = '', providers = ''] = key.slice(0, -FRAME.length).split(KINDS);
|
|
176
177
|
const split = (list: string) => list.split(SEPARATOR).filter(Boolean);
|
|
178
|
+
|
|
177
179
|
return { entities: split(entities), providers: split(providers) };
|
|
178
180
|
}
|