akanjs 2.2.7 → 2.2.9
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/CHANGELOG.md +11 -2
- package/client/clientRuntime.ts +3 -0
- package/client/csrTypes.ts +1 -0
- package/client/makePageProto.tsx +5 -2
- package/client/translator.ts +7 -4
- package/common/fileUpload.ts +1 -1
- package/common/index.ts +5 -1
- package/constant/getDefault.ts +1 -1
- package/dictionary/base.dictionary.ts +0 -1
- package/fetch/client/fetchClient.ts +21 -24
- package/fetch/client/wsClient.ts +8 -0
- package/fetch/serializer/fetch.serializer.ts +1 -0
- package/package.json +1 -5
- package/server/hmr/devHmrController.ts +1 -0
- package/server/routeTreeBuilder.ts +1 -0
- package/server/ssrFromRscRenderer.tsx +34 -12
- package/server/ssrTypes.ts +5 -6
- package/service/base.service.ts +0 -4
- package/service/injectInfo.ts +49 -12
- package/service/predefinedAdaptor/cache.adaptor.ts +13 -0
- package/service/predefinedAdaptor/database.adaptor.ts +74 -16
- package/service/predefinedAdaptor/solidCache.adaptor.ts +23 -0
- package/signal/base.signal.ts +0 -5
- package/signal/serializer/fetch.serializer.ts +1 -0
- package/signal/types.ts +3 -0
- package/store/action.ts +15 -3
- package/store/storeInstance.ts +50 -3
- package/types/client/csrTypes.d.ts +1 -0
- package/types/client/translator.d.ts +1 -0
- package/types/common/fileUpload.d.ts +1 -1
- package/types/common/index.d.ts +1 -1
- package/types/server/ssrTypes.d.ts +5 -6
- package/types/service/base.service.d.ts +0 -1
- package/types/service/injectInfo.d.ts +8 -2
- package/types/service/predefinedAdaptor/cache.adaptor.d.ts +6 -0
- package/types/service/predefinedAdaptor/database.adaptor.d.ts +3 -1
- package/types/service/predefinedAdaptor/solidCache.adaptor.d.ts +3 -0
- package/types/signal/base.signal.d.ts +0 -3
- package/types/signal/types.d.ts +3 -0
- package/types/ui/Dialog/Modal.d.ts +1 -1
- package/types/ui/Dialog/index.d.ts +1 -1
- package/types/ui/Modal.d.ts +1 -12
- package/types/ui/System/CSR.d.ts +2 -2
- package/types/ui/System/Client.d.ts +5 -4
- package/types/ui/System/Common.d.ts +2 -0
- package/types/ui/System/SSR.d.ts +2 -2
- package/ui/Dialog/Modal.tsx +181 -70
- package/ui/Dialog/Provider.tsx +3 -6
- package/ui/Modal.tsx +0 -44
- package/ui/System/CSR.tsx +9 -1
- package/ui/System/Client.tsx +27 -62
- package/ui/System/Common.tsx +2 -0
- package/ui/System/SSR.tsx +9 -1
- package/webkit/bootCsr.tsx +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,8 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
### Patch Changes
|
|
6
6
|
|
|
7
|
-
- fix: base dictionary translation failed in some cases
|
|
8
|
-
- fix: file upload contract workaround on shared Field.Img component
|
|
7
|
+
- bf51564: fix: base dictionary translation failed in some cases
|
|
8
|
+
- bf51564: fix: file upload contract workaround on shared Field.Img component
|
|
9
|
+
|
|
10
|
+
## 2.2.5
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- d636456: add rich Map methods on memory() helper service
|
|
15
|
+
- a1ee4e8: fill nested constant defaults for arrays on document save and load, normalize date fields to a consistent epoch representation on store (accepting legacy ISO-string values on read), and correct falsy defaults in getDefault
|
|
16
|
+
- 5cdb05e: reverse dependency of file upload api
|
|
17
|
+
- a7da50e: remove dependency from radix dialog
|
|
9
18
|
|
|
10
19
|
## 2.2.3
|
|
11
20
|
|
package/client/clientRuntime.ts
CHANGED
|
@@ -96,6 +96,9 @@ export const msg = new Proxy({} as ClientRuntime["msg"], {
|
|
|
96
96
|
get(_target, prop, receiver) {
|
|
97
97
|
return Reflect.get(getClientRuntime().msg, prop, receiver);
|
|
98
98
|
},
|
|
99
|
+
set(_target, prop, value) {
|
|
100
|
+
return Reflect.set(getClientRuntime().msg, prop, value);
|
|
101
|
+
},
|
|
99
102
|
});
|
|
100
103
|
|
|
101
104
|
const ErrTarget = function AkanClientRuntimeErr(...args: ConstructorParameters<RuntimeErr>) {
|
package/client/csrTypes.ts
CHANGED
package/client/makePageProto.tsx
CHANGED
|
@@ -25,8 +25,11 @@ const getPageInfo = (): { locale: string; path: string } => {
|
|
|
25
25
|
const localeSet = new Set(locales);
|
|
26
26
|
if (getEnv().side !== "server") {
|
|
27
27
|
const [, firstSegment = "", ...rest] = window.location.pathname.split("/");
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
const hasLocalePrefix = localeSet.has(firstSegment);
|
|
29
|
+
|
|
30
|
+
const activeLocale = Translator.getActiveLocale();
|
|
31
|
+
const locale = activeLocale ?? (hasLocalePrefix ? firstSegment : defaultLocale);
|
|
32
|
+
return { locale, path: hasLocalePrefix ? `/${rest.join("/")}` : window.location.pathname };
|
|
30
33
|
}
|
|
31
34
|
const h = headers();
|
|
32
35
|
|
package/client/translator.ts
CHANGED
|
@@ -44,6 +44,12 @@ export class Translator {
|
|
|
44
44
|
static getActiveLocale(): string | undefined {
|
|
45
45
|
return getTranslatorState().activeLocale;
|
|
46
46
|
}
|
|
47
|
+
static translateByLocale(lang: string, key: string, param?: Record<string, string | number>): string {
|
|
48
|
+
const dictionary = getTranslatorState().langDictionaryMap.get(lang);
|
|
49
|
+
if (!dictionary) return key;
|
|
50
|
+
const msg = (pathGet(key, dictionary, ".", { t: key }) as { t: string }).t;
|
|
51
|
+
return param ? msg.replace(/{([^}]+)}/g, (_, key: string) => param[key] as string) : msg;
|
|
52
|
+
}
|
|
47
53
|
|
|
48
54
|
static seed(lang: string, dict: Dictionary | undefined) {
|
|
49
55
|
if (!dict) return;
|
|
@@ -58,10 +64,7 @@ export class Translator {
|
|
|
58
64
|
state.langDictionaryMap.set(lang, existingDictionary);
|
|
59
65
|
}
|
|
60
66
|
translate(lang: string, key: string, param?: Record<string, string | number>): string {
|
|
61
|
-
|
|
62
|
-
if (!dictionary) return key;
|
|
63
|
-
const msg = (pathGet(key, dictionary, ".", { t: key }) as { t: string }).t;
|
|
64
|
-
return param ? msg.replace(/{([^}]+)}/g, (_, key: string) => param[key] as string) : msg;
|
|
67
|
+
return Translator.translateByLocale(lang, key, param);
|
|
65
68
|
}
|
|
66
69
|
async getDictionary(lang: string) {
|
|
67
70
|
const dictionary = getTranslatorState().langDictionaryMap.get(lang);
|
package/common/fileUpload.ts
CHANGED
package/common/index.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
export { applyMixins } from "./applyMixins";
|
|
2
2
|
export { capitalize } from "./capitalize";
|
|
3
3
|
export { deepObjectify } from "./deepObjectify";
|
|
4
|
-
export
|
|
4
|
+
export {
|
|
5
|
+
type FileUploadCapability,
|
|
6
|
+
fileUploadContract,
|
|
7
|
+
resolveFileUploadCapability,
|
|
8
|
+
} from "./fileUpload";
|
|
5
9
|
export { formatNumber } from "./formatNumber";
|
|
6
10
|
export { formatPhone } from "./formatPhone";
|
|
7
11
|
export { getAllPropertyDescriptors } from "./getAllPropertyDescriptors";
|
package/constant/getDefault.ts
CHANGED
|
@@ -6,7 +6,7 @@ export const getDefault = <T>(fieldObj: FieldObject): DefaultOf<T> => {
|
|
|
6
6
|
const result: Record<string, unknown> = {};
|
|
7
7
|
for (const [key, field] of Object.entries(fieldObj)) {
|
|
8
8
|
if (field.fieldType === "hidden") result[key] = null;
|
|
9
|
-
else if (field.default) {
|
|
9
|
+
else if (field.default !== undefined && field.default !== null) {
|
|
10
10
|
if (typeof field.default === "function") result[key] = (field.default as () => object)();
|
|
11
11
|
else result[key] = field.default as object;
|
|
12
12
|
} else if (field.isArray) result[key] = [];
|
|
@@ -20,7 +20,6 @@ export const baseDictionary = serviceDictionary(["en", "ko"])
|
|
|
20
20
|
.arg((t) => ({
|
|
21
21
|
id: t(["ID", "아이디"]),
|
|
22
22
|
})),
|
|
23
|
-
cleanup: fn(["Cleanup", "Cleanup"]).desc(["Cleanup operation", "정리 작업"]),
|
|
24
23
|
wsPing: fn(["Socket.io Ping", "Socket.io Ping"])
|
|
25
24
|
.desc(["Socket.io Ping test", "Socket.io Ping 테스트"])
|
|
26
25
|
.arg((t) => ({
|
|
@@ -1,12 +1,5 @@
|
|
|
1
1
|
import { DataList, getEnv, PrimitiveRegistry, type PromiseOrObject } from "akanjs/base";
|
|
2
|
-
import {
|
|
3
|
-
capitalize,
|
|
4
|
-
type FetchPolicy,
|
|
5
|
-
type FileUploadSerializedSignal,
|
|
6
|
-
fileUploadContract,
|
|
7
|
-
Logger,
|
|
8
|
-
resolveFileUploadCapability,
|
|
9
|
-
} from "akanjs/common";
|
|
2
|
+
import { capitalize, type FetchPolicy, fileUploadContract, Logger, resolveFileUploadCapability } from "akanjs/common";
|
|
10
3
|
import { type BaseInsight, type BaseObject, ConstantRegistry, deserialize, serialize } from "akanjs/constant";
|
|
11
4
|
import type {
|
|
12
5
|
DatabaseSignal,
|
|
@@ -180,6 +173,10 @@ export class FetchClient {
|
|
|
180
173
|
if (!handler) throw new Error(`${owner} requires fetch handler "${key}", but it is not registered`);
|
|
181
174
|
return handler as T;
|
|
182
175
|
}
|
|
176
|
+
#setHandlerFactory(key: string, factory: FetchHandlerFactory) {
|
|
177
|
+
this.#handlerFactory.set(key, factory);
|
|
178
|
+
delete this.#handlerStore[key];
|
|
179
|
+
}
|
|
183
180
|
connect() {
|
|
184
181
|
this.ws.connect();
|
|
185
182
|
}
|
|
@@ -263,15 +260,15 @@ export class FetchClient {
|
|
|
263
260
|
#registerEndpoint(key: string, endpoint: SerializedEndpoint, prefix?: string) {
|
|
264
261
|
switch (endpoint.type) {
|
|
265
262
|
case "query": {
|
|
266
|
-
this.#
|
|
263
|
+
this.#setHandlerFactory(key, () => this.#makeHttpFn(key, endpoint, prefix));
|
|
267
264
|
return;
|
|
268
265
|
}
|
|
269
266
|
case "mutation": {
|
|
270
|
-
this.#
|
|
267
|
+
this.#setHandlerFactory(key, () => this.#makeHttpFn(key, endpoint, prefix));
|
|
271
268
|
return;
|
|
272
269
|
}
|
|
273
270
|
case "pubsub": {
|
|
274
|
-
this.#
|
|
271
|
+
this.#setHandlerFactory(`subscribe${capitalize(key)}`, () => {
|
|
275
272
|
const roomArgs = endpoint.args.filter((arg) => arg.type === "room");
|
|
276
273
|
const roomArgLength = roomArgs.length;
|
|
277
274
|
const serializerMap = this.#makeArgSerializer(endpoint.args);
|
|
@@ -299,7 +296,7 @@ export class FetchClient {
|
|
|
299
296
|
return;
|
|
300
297
|
}
|
|
301
298
|
case "message": {
|
|
302
|
-
this.#
|
|
299
|
+
this.#setHandlerFactory(key, () => {
|
|
303
300
|
const msgArgs = endpoint.args.filter((arg) => arg.type === "msg");
|
|
304
301
|
const msgArgLength = msgArgs.length;
|
|
305
302
|
const serializerMap = this.#makeArgSerializer(endpoint.args);
|
|
@@ -309,7 +306,7 @@ export class FetchClient {
|
|
|
309
306
|
this.ws.emit(key, data);
|
|
310
307
|
};
|
|
311
308
|
});
|
|
312
|
-
this.#
|
|
309
|
+
this.#setHandlerFactory(`listen${capitalize(key)}`, () => {
|
|
313
310
|
const parseReturn = this.#makeReturnParser(endpoint.returns);
|
|
314
311
|
const wrappedListeners = new WeakMap<(data: unknown) => void, (data: unknown) => void>();
|
|
315
312
|
return ((handleEvent: (data: unknown) => void, fetchPolicy: FetchPolicy = {}) => {
|
|
@@ -414,11 +411,11 @@ export class FetchClient {
|
|
|
414
411
|
};
|
|
415
412
|
const endpoint = FetchClient.getBaseEndpoint(refName, signal);
|
|
416
413
|
Object.entries(endpoint).forEach(([key, value]) => {
|
|
417
|
-
this.#
|
|
414
|
+
this.#setHandlerFactory(key, () => this.#makeHttpFn(key, value, signal.prefix));
|
|
418
415
|
});
|
|
419
416
|
|
|
420
417
|
if (signal.cruGuards) {
|
|
421
|
-
this.#
|
|
418
|
+
this.#setHandlerFactory(
|
|
422
419
|
names.viewModel,
|
|
423
420
|
() =>
|
|
424
421
|
(async (id: string, option?: FetchPolicy) => {
|
|
@@ -432,7 +429,7 @@ export class FetchClient {
|
|
|
432
429
|
};
|
|
433
430
|
}) as FetchHandler,
|
|
434
431
|
);
|
|
435
|
-
this.#
|
|
432
|
+
this.#setHandlerFactory(
|
|
436
433
|
names.getModelView,
|
|
437
434
|
() =>
|
|
438
435
|
(async (id: string, option?: FetchPolicy) => {
|
|
@@ -441,7 +438,7 @@ export class FetchClient {
|
|
|
441
438
|
return { refName, [`${refName}Obj`]: modelObj, [`${refName}ViewAt`]: new Date() };
|
|
442
439
|
}) as FetchHandler,
|
|
443
440
|
);
|
|
444
|
-
this.#
|
|
441
|
+
this.#setHandlerFactory(
|
|
445
442
|
names.editModel,
|
|
446
443
|
() =>
|
|
447
444
|
(async (id: string, option?: FetchPolicy) => {
|
|
@@ -455,7 +452,7 @@ export class FetchClient {
|
|
|
455
452
|
};
|
|
456
453
|
}) as FetchHandler,
|
|
457
454
|
);
|
|
458
|
-
this.#
|
|
455
|
+
this.#setHandlerFactory(
|
|
459
456
|
names.getModelEdit,
|
|
460
457
|
() =>
|
|
461
458
|
(async (id: string, option?: FetchPolicy) => {
|
|
@@ -464,7 +461,7 @@ export class FetchClient {
|
|
|
464
461
|
return { refName, [`${refName}Obj`]: modelObj, [`${refName}ViewAt`]: new Date() };
|
|
465
462
|
}) as FetchHandler,
|
|
466
463
|
);
|
|
467
|
-
this.#
|
|
464
|
+
this.#setHandlerFactory(
|
|
468
465
|
names.mergeModel,
|
|
469
466
|
() =>
|
|
470
467
|
(async (modelOrId: string | { id: string }, data: UnknownRecord, option?: FetchPolicy) => {
|
|
@@ -475,11 +472,11 @@ export class FetchClient {
|
|
|
475
472
|
);
|
|
476
473
|
}
|
|
477
474
|
|
|
478
|
-
this.#
|
|
475
|
+
this.#setHandlerFactory(
|
|
479
476
|
names.addModelFiles,
|
|
480
477
|
() =>
|
|
481
478
|
(async (fileList: FileList, parentId?: string, option?: FetchPolicy) => {
|
|
482
|
-
const cap = resolveFileUploadCapability(this.serializedSignal
|
|
479
|
+
const cap = resolveFileUploadCapability(this.serializedSignal);
|
|
483
480
|
const endpoint = cap ? this.serializedSignal[cap.refName]?.endpoint[cap.endpointKey] : undefined;
|
|
484
481
|
if (!cap || !endpoint)
|
|
485
482
|
throw new Error(
|
|
@@ -536,12 +533,12 @@ export class FetchClient {
|
|
|
536
533
|
|
|
537
534
|
const endpoint = FetchClient.getEndpointFromSlice(refName, suffix, slice);
|
|
538
535
|
Object.entries(endpoint).forEach(([key, value]) => {
|
|
539
|
-
this.#
|
|
536
|
+
this.#setHandlerFactory(key, () => this.#makeHttpFn(key, value, prefix));
|
|
540
537
|
});
|
|
541
538
|
|
|
542
539
|
const argLength = slice.args.length;
|
|
543
540
|
this.slice[sliceName] = { refName, sliceName, argLength };
|
|
544
|
-
this.#
|
|
541
|
+
this.#setHandlerFactory(names.init, () => async (...argData: unknown[]) => {
|
|
545
542
|
const cnst = ConstantRegistry.getDatabase(refName);
|
|
546
543
|
const queryArgs = normalizeQueryArgs(
|
|
547
544
|
Array.from({ length: Math.min(argData.length, argLength) }, (_, idx) => argData[idx]),
|
|
@@ -583,7 +580,7 @@ export class FetchClient {
|
|
|
583
580
|
[`${refName}Insight${capSuffix}`]: modelInsight,
|
|
584
581
|
};
|
|
585
582
|
});
|
|
586
|
-
this.#
|
|
583
|
+
this.#setHandlerFactory(names.getInit, () => async (...args: unknown[]) => {
|
|
587
584
|
const initFn = this.#requireHandler<(...args: unknown[]) => Promise<Record<string, unknown>>>(
|
|
588
585
|
names.init,
|
|
589
586
|
names.getInit,
|
package/fetch/client/wsClient.ts
CHANGED
|
@@ -53,6 +53,7 @@ export class WsClient {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
connect() {
|
|
56
|
+
if (this.#ws && this.#ws.readyState !== WebSocket.CLOSED) return;
|
|
56
57
|
this.logger.debug(`Connecting to ${this.url}`);
|
|
57
58
|
this.#destroyed = false;
|
|
58
59
|
this.#reconnectAttempts = 0;
|
|
@@ -201,9 +202,15 @@ export class WsClient {
|
|
|
201
202
|
const hasRoom = roomSub ? roomSub.listener.size > 0 : false;
|
|
202
203
|
return hasGeneric || hasRoom;
|
|
203
204
|
}
|
|
205
|
+
#warnNotConnected(action: "emit" | "subscribe", key: string) {
|
|
206
|
+
console.warn(
|
|
207
|
+
`[akanjs] WebSocket is not connected. Call fetch.instance.connect() or enable root layout "wsConnect" before ${action} "${key}".`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
204
210
|
emit(key: string, data: WsRequestPayload) {
|
|
205
211
|
if (this.#ws?.readyState !== WebSocket.OPEN) {
|
|
206
212
|
this.logger.warn("WebSocket not connected");
|
|
213
|
+
this.#warnNotConnected("emit", key);
|
|
207
214
|
return this;
|
|
208
215
|
}
|
|
209
216
|
const payload: WebsocketReqData = { key, data: Array.isArray(data) ? data : [data] };
|
|
@@ -212,6 +219,7 @@ export class WsClient {
|
|
|
212
219
|
}
|
|
213
220
|
subscribe(option: { key: string; data: unknown[]; handleEvent: (data: unknown) => void }) {
|
|
214
221
|
const roomId = WsClient.makeRoomId(option.key, option.data);
|
|
222
|
+
if (!this.#ws) this.#warnNotConnected("subscribe", option.key);
|
|
215
223
|
if (!this.#roomSubscribeMap.has(roomId)) {
|
|
216
224
|
this.#roomSubscribeMap.set(roomId, { key: option.key, data: option.data, listener: new Set() });
|
|
217
225
|
if (this.#ws?.readyState === WebSocket.OPEN) {
|
|
@@ -67,6 +67,7 @@ export class FetchSerializer {
|
|
|
67
67
|
...(endpointInfo.signalOption.globalPrefix !== undefined
|
|
68
68
|
? { globalPrefix: endpointInfo.signalOption.globalPrefix }
|
|
69
69
|
: {}),
|
|
70
|
+
...(endpointInfo.signalOption.fileUpload ? { fileUpload: true } : {}),
|
|
70
71
|
...(guards?.length ? { guards } : {}),
|
|
71
72
|
};
|
|
72
73
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akanjs",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.9",
|
|
4
4
|
"sourceType": "module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -175,7 +175,6 @@
|
|
|
175
175
|
"@capgo/capacitor-updater": "^8.46.1",
|
|
176
176
|
"@libsql/client": "^0.17.3",
|
|
177
177
|
"@playwright/test": "^1.60.0",
|
|
178
|
-
"@radix-ui/react-dialog": "^1.1.15",
|
|
179
178
|
"@react-spring/web": "^10.1.0",
|
|
180
179
|
"@use-gesture/react": "^10.3.1",
|
|
181
180
|
"bullmq": "^5.76.10",
|
|
@@ -250,9 +249,6 @@
|
|
|
250
249
|
"@playwright/test": {
|
|
251
250
|
"optional": true
|
|
252
251
|
},
|
|
253
|
-
"@radix-ui/react-dialog": {
|
|
254
|
-
"optional": true
|
|
255
|
-
},
|
|
256
252
|
"@react-spring/web": {
|
|
257
253
|
"optional": true
|
|
258
254
|
},
|
|
@@ -362,6 +362,7 @@ export class DevHmrController {
|
|
|
362
362
|
`${path.sep}pkgs${path.sep}akanjs${path.sep}server${path.sep}src${path.sep}ssrFromRscRenderer.tsx`,
|
|
363
363
|
];
|
|
364
364
|
if (files.some((file) => runtimeRoots.some((needle) => path.resolve(file).includes(needle)))) return true;
|
|
365
|
+
if (files.some((file) => path.basename(file).endsWith(".signal.ts"))) return true;
|
|
365
366
|
|
|
366
367
|
return (
|
|
367
368
|
routeIds === undefined &&
|
|
@@ -69,7 +69,6 @@ export class SsrFromRscRenderer {
|
|
|
69
69
|
|
|
70
70
|
const renderHtml = () =>
|
|
71
71
|
renderToReadableStream(<Root />, {
|
|
72
|
-
bootstrapModules: input.bootstrapModules,
|
|
73
72
|
bootstrapScriptContent: bootstrap,
|
|
74
73
|
});
|
|
75
74
|
const htmlStream =
|
|
@@ -77,6 +76,7 @@ export class SsrFromRscRenderer {
|
|
|
77
76
|
|
|
78
77
|
const withHeadScripts = SsrFromRscRenderer.#injectHeadScriptsIntoHead(htmlStream, {
|
|
79
78
|
importmap: input.importmap,
|
|
79
|
+
bootstrapModules: input.bootstrapModules,
|
|
80
80
|
theme: input.theme,
|
|
81
81
|
injectThemeInitScript: input.injectThemeInitScript,
|
|
82
82
|
});
|
|
@@ -84,6 +84,7 @@ export class SsrFromRscRenderer {
|
|
|
84
84
|
return SsrFromRscRenderer.#appendRscScriptsAfterHtml(
|
|
85
85
|
withHeadScripts,
|
|
86
86
|
SsrFromRscRenderer.#sanitizeFlightForClient(rscForClient),
|
|
87
|
+
input.bootstrapModules,
|
|
87
88
|
input.request,
|
|
88
89
|
);
|
|
89
90
|
}
|
|
@@ -124,32 +125,35 @@ export class SsrFromRscRenderer {
|
|
|
124
125
|
* tag in the outgoing HTML stream.
|
|
125
126
|
*
|
|
126
127
|
* We do this as a stream transform (rather than as a React child inside
|
|
127
|
-
* `<head>`)
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* browser starts a module script fetch for a preload, the document's
|
|
132
|
-
* "allow-import-maps" bit flips to false and no further importmap can be
|
|
133
|
-
* acquired. By writing the importmap before any of React's own `<head>`
|
|
134
|
-
* output reaches the wire, we guarantee the browser sees it first.
|
|
128
|
+
* `<head>`) so importmaps are acquired before any modulepreload can start.
|
|
129
|
+
* The spec is strict: once the browser starts a module script fetch for a
|
|
130
|
+
* preload, the document's "allow-import-maps" bit flips to false and no
|
|
131
|
+
* further importmap can be acquired.
|
|
135
132
|
*
|
|
136
133
|
* The transform operates on UTF-8 bytes until it has spliced the tag, then
|
|
137
134
|
* becomes a pure passthrough to avoid any further per-chunk overhead.
|
|
138
135
|
*/
|
|
139
136
|
static #injectHeadScriptsIntoHead(
|
|
140
137
|
stream: ReadableStream<Uint8Array>,
|
|
141
|
-
options: {
|
|
138
|
+
options: {
|
|
139
|
+
importmap?: Record<string, string>;
|
|
140
|
+
bootstrapModules?: string[];
|
|
141
|
+
theme?: AkanTheme;
|
|
142
|
+
injectThemeInitScript?: boolean;
|
|
143
|
+
},
|
|
142
144
|
): ReadableStream<Uint8Array> {
|
|
143
145
|
const encoder = new TextEncoder();
|
|
144
146
|
const decoder = new TextDecoder();
|
|
145
|
-
const { importmap, theme, injectThemeInitScript } = options;
|
|
147
|
+
const { importmap, bootstrapModules, theme, injectThemeInitScript } = options;
|
|
146
148
|
const htmlTheme = theme && theme !== "css" && theme !== "system" ? theme : undefined;
|
|
147
149
|
const importmapTag =
|
|
148
150
|
importmap && Object.keys(importmap).length > 0
|
|
149
151
|
? `<script type="importmap">${JSON.stringify({ imports: importmap })}</script>`
|
|
150
152
|
: "";
|
|
153
|
+
const modulePreloadTags = SsrFromRscRenderer.#createBootstrapModulePreloadTags(bootstrapModules);
|
|
151
154
|
const shouldInjectThemeScript = theme === "system" || (theme === undefined && injectThemeInitScript);
|
|
152
|
-
const
|
|
155
|
+
const themeInitTag = shouldInjectThemeScript ? SsrFromRscRenderer.#themeInitScript : "";
|
|
156
|
+
const tags = `${themeInitTag}${importmapTag}${modulePreloadTags}`;
|
|
153
157
|
if (!tags && !htmlTheme) return stream;
|
|
154
158
|
const htmlOpenRe = /<html(\s[^>]*)?>/i;
|
|
155
159
|
const headOpenRe = /<head(\s[^>]*)?>/i;
|
|
@@ -200,6 +204,20 @@ export class SsrFromRscRenderer {
|
|
|
200
204
|
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
201
205
|
}
|
|
202
206
|
|
|
207
|
+
static #createBootstrapModulePreloadTags(bootstrapModules?: string[]): string {
|
|
208
|
+
if (!bootstrapModules?.length) return "";
|
|
209
|
+
return bootstrapModules
|
|
210
|
+
.map((src) => `<link rel="modulepreload" href="${SsrFromRscRenderer.#escapeHtmlAttr(src)}">`)
|
|
211
|
+
.join("");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
static #createBootstrapModuleScriptTags(bootstrapModules?: string[]): string {
|
|
215
|
+
if (!bootstrapModules?.length) return "";
|
|
216
|
+
return bootstrapModules
|
|
217
|
+
.map((src) => `<script type="module" src="${SsrFromRscRenderer.#escapeHtmlAttr(src)}"></script>`)
|
|
218
|
+
.join("");
|
|
219
|
+
}
|
|
220
|
+
|
|
203
221
|
static #sanitizeFlightForClient(stream: ReadableStream<Uint8Array>): ReadableStream<Uint8Array> {
|
|
204
222
|
const decoder = new TextDecoder();
|
|
205
223
|
const encoder = new TextEncoder();
|
|
@@ -227,9 +245,11 @@ export class SsrFromRscRenderer {
|
|
|
227
245
|
static #appendRscScriptsAfterHtml(
|
|
228
246
|
htmlStream: ReadableStream<Uint8Array>,
|
|
229
247
|
rscClientStream: ReadableStream<Uint8Array>,
|
|
248
|
+
bootstrapModules?: string[],
|
|
230
249
|
request?: Request,
|
|
231
250
|
): ReadableStream<Uint8Array> {
|
|
232
251
|
const encoder = new TextEncoder();
|
|
252
|
+
const bootstrapModuleScripts = SsrFromRscRenderer.#createBootstrapModuleScriptTags(bootstrapModules);
|
|
233
253
|
|
|
234
254
|
return new ReadableStream<Uint8Array>({
|
|
235
255
|
start(controller) {
|
|
@@ -253,6 +273,8 @@ export class SsrFromRscRenderer {
|
|
|
253
273
|
reader.releaseLock();
|
|
254
274
|
}
|
|
255
275
|
|
|
276
|
+
if (bootstrapModuleScripts && !errored) controller.enqueue(encoder.encode(bootstrapModuleScripts));
|
|
277
|
+
|
|
256
278
|
const rscReader = rscClientStream.getReader();
|
|
257
279
|
try {
|
|
258
280
|
while (true) {
|
package/server/ssrTypes.ts
CHANGED
|
@@ -35,12 +35,11 @@ export interface SsrFromRscInput {
|
|
|
35
35
|
* guaranteeing one React instance across rscClient and every route
|
|
36
36
|
* chunk.
|
|
37
37
|
*
|
|
38
|
-
* Injection happens via a stream transform, not React children, because
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* modulepreload.
|
|
38
|
+
* Injection happens via a stream transform, not React children, because the
|
|
39
|
+
* spec is strict: import maps must be acquired before any module script fetch
|
|
40
|
+
* starts, including modulepreload. Akan writes bootstrap module preloads
|
|
41
|
+
* directly after this importmap and delays the executable module script until
|
|
42
|
+
* the Fizz HTML stream has completed.
|
|
44
43
|
*/
|
|
45
44
|
importmap?: Record<string, string>;
|
|
46
45
|
theme?: AkanTheme;
|
package/service/base.service.ts
CHANGED
|
@@ -9,10 +9,6 @@ export class BaseService extends serve("base" as const, ({ env, signal, memory }
|
|
|
9
9
|
publishPing() {
|
|
10
10
|
this.baseSignal.pubsubPing("ping");
|
|
11
11
|
}
|
|
12
|
-
async cleanup() {
|
|
13
|
-
if (!this.onCleanup) throw new Error("onCleanup is not defined");
|
|
14
|
-
await this.onCleanup();
|
|
15
|
-
}
|
|
16
12
|
}
|
|
17
13
|
|
|
18
14
|
export const srv = { base: new ServiceModel(BaseService) };
|
package/service/injectInfo.ts
CHANGED
|
@@ -254,20 +254,47 @@ export class InjectInfo<
|
|
|
254
254
|
enumerable: true,
|
|
255
255
|
});
|
|
256
256
|
} else if (injectInfo.isMap) {
|
|
257
|
+
const topic = `akan:memory:${injectInfo.parentRefName}`;
|
|
258
|
+
const getter = injectInfo.get as unknown as (value: unknown) => unknown;
|
|
259
|
+
const setter = injectInfo.set as unknown as (value: unknown) => string | number | Buffer;
|
|
260
|
+
const get = async (key: string) => {
|
|
261
|
+
const value = await cacheAdaptor.hget(topic, propKey, key);
|
|
262
|
+
return value === undefined || value === null ? undefined : getter(value);
|
|
263
|
+
};
|
|
264
|
+
const set = async (key: string, value: unknown) => {
|
|
265
|
+
const setValue = setter(value);
|
|
266
|
+
await cacheAdaptor.hset(topic, propKey, key, setValue);
|
|
267
|
+
};
|
|
257
268
|
Object.defineProperty(instance, propKey, {
|
|
258
269
|
value: {
|
|
259
|
-
get
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
270
|
+
get,
|
|
271
|
+
set,
|
|
272
|
+
delete: async (key: string) => {
|
|
273
|
+
await cacheAdaptor.hdelete(topic, propKey, key);
|
|
263
274
|
},
|
|
264
|
-
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
await
|
|
275
|
+
getOrInsert: async (key: string, value: unknown) => {
|
|
276
|
+
const existingValue = await get(key);
|
|
277
|
+
if (existingValue !== undefined) return existingValue;
|
|
278
|
+
await set(key, value);
|
|
279
|
+
return value;
|
|
268
280
|
},
|
|
269
|
-
|
|
270
|
-
await
|
|
281
|
+
getOrInsertComputed: async (key: string, compute: (key: string) => unknown | Promise<unknown>) => {
|
|
282
|
+
const existingValue = await get(key);
|
|
283
|
+
if (existingValue !== undefined) return existingValue;
|
|
284
|
+
const value = await compute(key);
|
|
285
|
+
await set(key, value);
|
|
286
|
+
return value;
|
|
287
|
+
},
|
|
288
|
+
keys: async () => await cacheAdaptor.hkeys(topic, propKey),
|
|
289
|
+
entries: async () => {
|
|
290
|
+
const entries = await cacheAdaptor.hentries(topic, propKey);
|
|
291
|
+
return entries.map(([key, value]) => [key, getter(value)]);
|
|
292
|
+
},
|
|
293
|
+
forEach: async (callback: (value: unknown, key: string) => void | Promise<void>) => {
|
|
294
|
+
for (const [key, value] of await cacheAdaptor.hentries(topic, propKey)) await callback(getter(value), key);
|
|
295
|
+
},
|
|
296
|
+
clear: async () => {
|
|
297
|
+
await cacheAdaptor.hclear(topic, propKey);
|
|
271
298
|
},
|
|
272
299
|
},
|
|
273
300
|
});
|
|
@@ -346,6 +373,7 @@ export const injectionBuilder = (parentRefName: string) => ({
|
|
|
346
373
|
const isMap = modelRef === Map;
|
|
347
374
|
if (isMap && !opts.of) throw new Error("of should be provided when modelRef is Map");
|
|
348
375
|
type FieldValue = never extends GetFn ? GetFieldValue<ValueRef, ExplicitType, MapValue> : ReturnType<GetFn>;
|
|
376
|
+
type MapFieldValue = never extends GetFn ? FieldToValue<MapValue> : ReturnType<GetFn>;
|
|
349
377
|
type IsNullable = DefaultValue extends never ? true : false;
|
|
350
378
|
type UseValue = IsNullable extends true ? FieldValue | null : FieldValue;
|
|
351
379
|
return new InjectInfo<
|
|
@@ -356,9 +384,18 @@ export const injectionBuilder = (parentRefName: string) => ({
|
|
|
356
384
|
: UseValue
|
|
357
385
|
: MapConstructor extends ValueRef
|
|
358
386
|
? {
|
|
359
|
-
get: (key: string) => Promise<
|
|
360
|
-
set: (key: string, value:
|
|
387
|
+
get: (key: string) => Promise<MapFieldValue | undefined>;
|
|
388
|
+
set: (key: string, value: MapFieldValue) => Promise<void>;
|
|
361
389
|
delete: (key: string) => Promise<void>;
|
|
390
|
+
getOrInsert: (key: string, value: MapFieldValue) => Promise<MapFieldValue>;
|
|
391
|
+
getOrInsertComputed: (
|
|
392
|
+
key: string,
|
|
393
|
+
compute: (key: string) => MapFieldValue | Promise<MapFieldValue>,
|
|
394
|
+
) => Promise<MapFieldValue>;
|
|
395
|
+
keys: () => Promise<string[]>;
|
|
396
|
+
entries: () => Promise<[string, MapFieldValue][]>;
|
|
397
|
+
forEach: (callback: (value: MapFieldValue, key: string) => void | Promise<void>) => Promise<void>;
|
|
398
|
+
clear: () => Promise<void>;
|
|
362
399
|
}
|
|
363
400
|
: { get: () => Promise<UseValue>; set: (value: UseValue) => Promise<void>; delete: () => Promise<void> },
|
|
364
401
|
never,
|
|
@@ -16,6 +16,9 @@ export interface CacheAdaptor {
|
|
|
16
16
|
): Promise<void>;
|
|
17
17
|
hget<T extends string | number | Buffer>(topic: string, key: string, subKey: string): Promise<T | undefined>;
|
|
18
18
|
hdelete(topic: string, key: string, subKey: string): Promise<void>;
|
|
19
|
+
hkeys(topic: string, key: string): Promise<string[]>;
|
|
20
|
+
hentries<T extends string | number | Buffer>(topic: string, key: string): Promise<[string, T][]>;
|
|
21
|
+
hclear(topic: string, key: string): Promise<void>;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
interface RedisEnv extends BaseEnv {
|
|
@@ -96,6 +99,16 @@ export class RedisCache
|
|
|
96
99
|
async hdelete(topic: string, key: string, subKey: string): Promise<void> {
|
|
97
100
|
await this.redis.hdel(`${topic}:${key}`, subKey);
|
|
98
101
|
}
|
|
102
|
+
async hkeys(topic: string, key: string): Promise<string[]> {
|
|
103
|
+
return await this.redis.hkeys(`${topic}:${key}`);
|
|
104
|
+
}
|
|
105
|
+
async hentries<T extends string | number | Buffer>(topic: string, key: string): Promise<[string, T][]> {
|
|
106
|
+
const values = await this.redis.hgetall(`${topic}:${key}`);
|
|
107
|
+
return Object.entries(values) as [string, T][];
|
|
108
|
+
}
|
|
109
|
+
async hclear(topic: string, key: string): Promise<void> {
|
|
110
|
+
await this.redis.del(`${topic}:${key}`);
|
|
111
|
+
}
|
|
99
112
|
getClient(): Redis {
|
|
100
113
|
return this.redis;
|
|
101
114
|
}
|