akanjs 2.2.8 → 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/client/clientRuntime.ts +3 -0
- package/client/csrTypes.ts +1 -0
- package/client/translator.ts +7 -4
- package/dictionary/base.dictionary.ts +0 -1
- package/fetch/client/fetchClient.ts +19 -15
- package/fetch/client/wsClient.ts +8 -0
- package/package.json +1 -1
- 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/signal/base.signal.ts +0 -5
- package/store/storeInstance.ts +50 -3
- package/types/client/csrTypes.d.ts +1 -0
- package/types/client/translator.d.ts +1 -0
- package/types/server/ssrTypes.d.ts +5 -6
- package/types/service/base.service.d.ts +0 -1
- package/types/signal/base.signal.d.ts +0 -3
- package/types/ui/System/CSR.d.ts +2 -2
- package/types/ui/System/Client.d.ts +3 -2
- package/types/ui/System/Common.d.ts +2 -0
- package/types/ui/System/SSR.d.ts +2 -2
- package/ui/System/CSR.tsx +9 -1
- package/ui/System/Client.tsx +8 -1
- package/ui/System/Common.tsx +2 -0
- package/ui/System/SSR.tsx +9 -1
- package/webkit/bootCsr.tsx +1 -0
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/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);
|
|
@@ -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) => ({
|
|
@@ -173,6 +173,10 @@ export class FetchClient {
|
|
|
173
173
|
if (!handler) throw new Error(`${owner} requires fetch handler "${key}", but it is not registered`);
|
|
174
174
|
return handler as T;
|
|
175
175
|
}
|
|
176
|
+
#setHandlerFactory(key: string, factory: FetchHandlerFactory) {
|
|
177
|
+
this.#handlerFactory.set(key, factory);
|
|
178
|
+
delete this.#handlerStore[key];
|
|
179
|
+
}
|
|
176
180
|
connect() {
|
|
177
181
|
this.ws.connect();
|
|
178
182
|
}
|
|
@@ -256,15 +260,15 @@ export class FetchClient {
|
|
|
256
260
|
#registerEndpoint(key: string, endpoint: SerializedEndpoint, prefix?: string) {
|
|
257
261
|
switch (endpoint.type) {
|
|
258
262
|
case "query": {
|
|
259
|
-
this.#
|
|
263
|
+
this.#setHandlerFactory(key, () => this.#makeHttpFn(key, endpoint, prefix));
|
|
260
264
|
return;
|
|
261
265
|
}
|
|
262
266
|
case "mutation": {
|
|
263
|
-
this.#
|
|
267
|
+
this.#setHandlerFactory(key, () => this.#makeHttpFn(key, endpoint, prefix));
|
|
264
268
|
return;
|
|
265
269
|
}
|
|
266
270
|
case "pubsub": {
|
|
267
|
-
this.#
|
|
271
|
+
this.#setHandlerFactory(`subscribe${capitalize(key)}`, () => {
|
|
268
272
|
const roomArgs = endpoint.args.filter((arg) => arg.type === "room");
|
|
269
273
|
const roomArgLength = roomArgs.length;
|
|
270
274
|
const serializerMap = this.#makeArgSerializer(endpoint.args);
|
|
@@ -292,7 +296,7 @@ export class FetchClient {
|
|
|
292
296
|
return;
|
|
293
297
|
}
|
|
294
298
|
case "message": {
|
|
295
|
-
this.#
|
|
299
|
+
this.#setHandlerFactory(key, () => {
|
|
296
300
|
const msgArgs = endpoint.args.filter((arg) => arg.type === "msg");
|
|
297
301
|
const msgArgLength = msgArgs.length;
|
|
298
302
|
const serializerMap = this.#makeArgSerializer(endpoint.args);
|
|
@@ -302,7 +306,7 @@ export class FetchClient {
|
|
|
302
306
|
this.ws.emit(key, data);
|
|
303
307
|
};
|
|
304
308
|
});
|
|
305
|
-
this.#
|
|
309
|
+
this.#setHandlerFactory(`listen${capitalize(key)}`, () => {
|
|
306
310
|
const parseReturn = this.#makeReturnParser(endpoint.returns);
|
|
307
311
|
const wrappedListeners = new WeakMap<(data: unknown) => void, (data: unknown) => void>();
|
|
308
312
|
return ((handleEvent: (data: unknown) => void, fetchPolicy: FetchPolicy = {}) => {
|
|
@@ -407,11 +411,11 @@ export class FetchClient {
|
|
|
407
411
|
};
|
|
408
412
|
const endpoint = FetchClient.getBaseEndpoint(refName, signal);
|
|
409
413
|
Object.entries(endpoint).forEach(([key, value]) => {
|
|
410
|
-
this.#
|
|
414
|
+
this.#setHandlerFactory(key, () => this.#makeHttpFn(key, value, signal.prefix));
|
|
411
415
|
});
|
|
412
416
|
|
|
413
417
|
if (signal.cruGuards) {
|
|
414
|
-
this.#
|
|
418
|
+
this.#setHandlerFactory(
|
|
415
419
|
names.viewModel,
|
|
416
420
|
() =>
|
|
417
421
|
(async (id: string, option?: FetchPolicy) => {
|
|
@@ -425,7 +429,7 @@ export class FetchClient {
|
|
|
425
429
|
};
|
|
426
430
|
}) as FetchHandler,
|
|
427
431
|
);
|
|
428
|
-
this.#
|
|
432
|
+
this.#setHandlerFactory(
|
|
429
433
|
names.getModelView,
|
|
430
434
|
() =>
|
|
431
435
|
(async (id: string, option?: FetchPolicy) => {
|
|
@@ -434,7 +438,7 @@ export class FetchClient {
|
|
|
434
438
|
return { refName, [`${refName}Obj`]: modelObj, [`${refName}ViewAt`]: new Date() };
|
|
435
439
|
}) as FetchHandler,
|
|
436
440
|
);
|
|
437
|
-
this.#
|
|
441
|
+
this.#setHandlerFactory(
|
|
438
442
|
names.editModel,
|
|
439
443
|
() =>
|
|
440
444
|
(async (id: string, option?: FetchPolicy) => {
|
|
@@ -448,7 +452,7 @@ export class FetchClient {
|
|
|
448
452
|
};
|
|
449
453
|
}) as FetchHandler,
|
|
450
454
|
);
|
|
451
|
-
this.#
|
|
455
|
+
this.#setHandlerFactory(
|
|
452
456
|
names.getModelEdit,
|
|
453
457
|
() =>
|
|
454
458
|
(async (id: string, option?: FetchPolicy) => {
|
|
@@ -457,7 +461,7 @@ export class FetchClient {
|
|
|
457
461
|
return { refName, [`${refName}Obj`]: modelObj, [`${refName}ViewAt`]: new Date() };
|
|
458
462
|
}) as FetchHandler,
|
|
459
463
|
);
|
|
460
|
-
this.#
|
|
464
|
+
this.#setHandlerFactory(
|
|
461
465
|
names.mergeModel,
|
|
462
466
|
() =>
|
|
463
467
|
(async (modelOrId: string | { id: string }, data: UnknownRecord, option?: FetchPolicy) => {
|
|
@@ -468,7 +472,7 @@ export class FetchClient {
|
|
|
468
472
|
);
|
|
469
473
|
}
|
|
470
474
|
|
|
471
|
-
this.#
|
|
475
|
+
this.#setHandlerFactory(
|
|
472
476
|
names.addModelFiles,
|
|
473
477
|
() =>
|
|
474
478
|
(async (fileList: FileList, parentId?: string, option?: FetchPolicy) => {
|
|
@@ -529,12 +533,12 @@ export class FetchClient {
|
|
|
529
533
|
|
|
530
534
|
const endpoint = FetchClient.getEndpointFromSlice(refName, suffix, slice);
|
|
531
535
|
Object.entries(endpoint).forEach(([key, value]) => {
|
|
532
|
-
this.#
|
|
536
|
+
this.#setHandlerFactory(key, () => this.#makeHttpFn(key, value, prefix));
|
|
533
537
|
});
|
|
534
538
|
|
|
535
539
|
const argLength = slice.args.length;
|
|
536
540
|
this.slice[sliceName] = { refName, sliceName, argLength };
|
|
537
|
-
this.#
|
|
541
|
+
this.#setHandlerFactory(names.init, () => async (...argData: unknown[]) => {
|
|
538
542
|
const cnst = ConstantRegistry.getDatabase(refName);
|
|
539
543
|
const queryArgs = normalizeQueryArgs(
|
|
540
544
|
Array.from({ length: Math.min(argData.length, argLength) }, (_, idx) => argData[idx]),
|
|
@@ -576,7 +580,7 @@ export class FetchClient {
|
|
|
576
580
|
[`${refName}Insight${capSuffix}`]: modelInsight,
|
|
577
581
|
};
|
|
578
582
|
});
|
|
579
|
-
this.#
|
|
583
|
+
this.#setHandlerFactory(names.getInit, () => async (...args: unknown[]) => {
|
|
580
584
|
const initFn = this.#requireHandler<(...args: unknown[]) => Promise<Record<string, unknown>>>(
|
|
581
585
|
names.init,
|
|
582
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) {
|
package/package.json
CHANGED
|
@@ -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/signal/base.signal.ts
CHANGED
|
@@ -18,11 +18,6 @@ export class BaseEndpoint extends endpoint(srv.base, ({ query, mutation, message
|
|
|
18
18
|
pingQuery: query(String, { nullable: true })
|
|
19
19
|
.search("id", String)
|
|
20
20
|
.exec((id) => `pingQuery: ${id}`),
|
|
21
|
-
cleanup: mutation(Boolean).exec(async function () {
|
|
22
|
-
if (process.env.NODE_ENV !== "test") throw new Error("cleanup is only available in test environment");
|
|
23
|
-
await this.baseService.cleanup();
|
|
24
|
-
return true;
|
|
25
|
-
}),
|
|
26
21
|
wsPing: message(String)
|
|
27
22
|
.msg("data", String, { nullable: true })
|
|
28
23
|
.exec((data) => `wsPing: ${data}`),
|
package/store/storeInstance.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ACTION_META, STATE_DERIVED_META, STATE_INIT_META } from "akanjs/base";
|
|
2
|
-
import {
|
|
2
|
+
import { Translator } from "akanjs/client/translator";
|
|
3
|
+
import { capitalize, Logger, parseAkanI18nEnv } from "akanjs/common";
|
|
3
4
|
import { produce } from "immer";
|
|
4
5
|
import type { RefObject } from "react";
|
|
5
6
|
import { useEffect, useRef, useSyncExternalStore } from "./hooks";
|
|
@@ -9,6 +10,7 @@ import { evaluateInitializers, type SearchParamsState, type StateDerivedMeta } f
|
|
|
9
10
|
|
|
10
11
|
type StoreStateRecord = Record<string, unknown>;
|
|
11
12
|
type StoreAction = (...args: unknown[]) => unknown;
|
|
13
|
+
type TranslationParam = Record<string, string | number>;
|
|
12
14
|
|
|
13
15
|
type SliceActionKey =
|
|
14
16
|
| "initModel"
|
|
@@ -20,6 +22,27 @@ type SliceActionKey =
|
|
|
20
22
|
| "setQueryArgsOfModel"
|
|
21
23
|
| "setSortOfModel";
|
|
22
24
|
|
|
25
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
26
|
+
Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
27
|
+
|
|
28
|
+
const getActionErrorKey = (error: unknown) => {
|
|
29
|
+
if (typeof error === "string") return error;
|
|
30
|
+
if (!isRecord(error)) return String(error);
|
|
31
|
+
if (typeof error.error === "string") return error.error;
|
|
32
|
+
if (typeof error.message === "string") return error.message;
|
|
33
|
+
return String(error);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const getActionErrorData = (error: unknown): TranslationParam | undefined => {
|
|
37
|
+
if (!isRecord(error) || !isRecord(error.data)) return undefined;
|
|
38
|
+
const data = Object.fromEntries(
|
|
39
|
+
Object.entries(error.data).filter((entry): entry is [string, string | number] =>
|
|
40
|
+
["string", "number"].includes(typeof entry[1]),
|
|
41
|
+
),
|
|
42
|
+
);
|
|
43
|
+
return Object.keys(data).length ? data : undefined;
|
|
44
|
+
};
|
|
45
|
+
|
|
23
46
|
export type ReactAPI = {
|
|
24
47
|
useSyncExternalStore: <T>(
|
|
25
48
|
subscribe: (onStoreChange: () => void) => () => void,
|
|
@@ -190,12 +213,36 @@ export class StoreInstance {
|
|
|
190
213
|
this.do[k] = async (...args: unknown[]) => {
|
|
191
214
|
Logger.verbose(`${k} action loading...`);
|
|
192
215
|
const start = Date.now();
|
|
193
|
-
|
|
194
|
-
|
|
216
|
+
try {
|
|
217
|
+
const result = await (this.#ctx[k] as StoreAction)(...args);
|
|
218
|
+
Logger.verbose(`=> ${k} action dispatched (${Date.now() - start}ms)`);
|
|
219
|
+
return result;
|
|
220
|
+
} catch (error) {
|
|
221
|
+
this.#showActionErrorMessage(k, error);
|
|
222
|
+
Logger.error(`${k} action error return: ${error instanceof Error ? error.message : String(error)}`);
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
195
225
|
};
|
|
196
226
|
}
|
|
197
227
|
}
|
|
198
228
|
|
|
229
|
+
#showActionErrorMessage(actionKey: string, error: unknown) {
|
|
230
|
+
const showMessage = this.#ctx.showMessage;
|
|
231
|
+
if (typeof showMessage !== "function") return;
|
|
232
|
+
try {
|
|
233
|
+
const lang = Translator.getActiveLocale() ?? parseAkanI18nEnv().defaultLocale;
|
|
234
|
+
const errorKey = getActionErrorKey(error);
|
|
235
|
+
const content = Translator.translateByLocale(lang, errorKey, getActionErrorData(error));
|
|
236
|
+
showMessage({ type: "error", key: actionKey, duration: 3, content });
|
|
237
|
+
} catch (messageError) {
|
|
238
|
+
Logger.warn(
|
|
239
|
+
`Failed to show ${actionKey} action error message: ${
|
|
240
|
+
messageError instanceof Error ? messageError.message : String(messageError)
|
|
241
|
+
}`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
199
246
|
#buildSlices(store: RootStoreCls) {
|
|
200
247
|
Object.entries(store.slice).forEach(([refName, sliceObj]) => {
|
|
201
248
|
Object.entries(sliceObj).forEach(([suffix, serializedSlice]) => {
|
|
@@ -11,6 +11,7 @@ export declare class Translator {
|
|
|
11
11
|
hasDictionary(lang: string): boolean;
|
|
12
12
|
static setActiveLocale(lang: string | undefined): void;
|
|
13
13
|
static getActiveLocale(): string | undefined;
|
|
14
|
+
static translateByLocale(lang: string, key: string, param?: Record<string, string | number>): string;
|
|
14
15
|
static seed(lang: string, dict: Dictionary | undefined): void;
|
|
15
16
|
translate(lang: string, key: string, param?: Record<string, string | number>): string;
|
|
16
17
|
getDictionary(lang: string): Promise<Dictionary>;
|
|
@@ -34,12 +34,11 @@ export interface SsrFromRscInput {
|
|
|
34
34
|
* guaranteeing one React instance across rscClient and every route
|
|
35
35
|
* chunk.
|
|
36
36
|
*
|
|
37
|
-
* Injection happens via a stream transform, not React children, because
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* modulepreload.
|
|
37
|
+
* Injection happens via a stream transform, not React children, because the
|
|
38
|
+
* spec is strict: import maps must be acquired before any module script fetch
|
|
39
|
+
* starts, including modulepreload. Akan writes bootstrap module preloads
|
|
40
|
+
* directly after this importmap and delays the executable module script until
|
|
41
|
+
* the Fizz HTML stream has completed.
|
|
43
42
|
*/
|
|
44
43
|
importmap?: Record<string, string>;
|
|
45
44
|
theme?: AkanTheme;
|
|
@@ -8,7 +8,6 @@ declare const BaseService_base: import("./serve.d.ts").ServiceCls<"base", {}, {
|
|
|
8
8
|
}>;
|
|
9
9
|
export declare class BaseService extends BaseService_base {
|
|
10
10
|
publishPing(): void;
|
|
11
|
-
cleanup(): Promise<void>;
|
|
12
11
|
}
|
|
13
12
|
export declare const srv: {
|
|
14
13
|
base: ServiceModel<typeof BaseService, any, any, {
|
|
@@ -18,9 +18,6 @@ declare const BaseEndpoint_base: import("./endpoint.d.ts").EndpointCls<import("a
|
|
|
18
18
|
pingQuery: import("./endpointInfo.d.ts").EndpointInfo<"query", {
|
|
19
19
|
baseService: import("akanjs/service").BaseService;
|
|
20
20
|
}, [string], [arg?: string | null | undefined], [], [arg: string | undefined], StringConstructor, string, string, true>;
|
|
21
|
-
cleanup: import("./endpointInfo.d.ts").EndpointInfo<"mutation", {
|
|
22
|
-
baseService: import("akanjs/service").BaseService;
|
|
23
|
-
}, [], [], [], [], BooleanConstructor, boolean, Promise<boolean>, false>;
|
|
24
21
|
wsPing: import("./endpointInfo.d.ts").EndpointInfo<"message", {
|
|
25
22
|
baseService: import("akanjs/service").BaseService;
|
|
26
23
|
}, [string], [arg?: string | null | undefined], [], [arg: string | undefined], StringConstructor, string, string, false>;
|
package/types/ui/System/CSR.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export declare const CSR: {
|
|
|
5
5
|
({ children }: {
|
|
6
6
|
children: ReactNode;
|
|
7
7
|
}): import("react/jsx-runtime").JSX.Element;
|
|
8
|
-
Provider: ({ className, appName, params, head, manifest, env, theme, prefix, children, gaTrackingId, fonts, layoutStyle, reconnect, of, }: CSRProviderProps) => import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
Provider: ({ className, appName, params, head, manifest, env, theme, prefix, children, gaTrackingId, fonts, layoutStyle, reconnect, wsConnect, of, }: CSRProviderProps) => import("react/jsx-runtime").JSX.Element;
|
|
9
9
|
Wrapper: ({ children, lang, head, manifest, fonts, appName, className, prefix, layoutStyle, }: CSRWrapperProps) => import("react/jsx-runtime").JSX.Element;
|
|
10
10
|
Inner: () => import("react/jsx-runtime").JSX.Element;
|
|
11
11
|
Bridge: ({ lang, prefix }: CSRBridgeProps) => null;
|
|
@@ -13,7 +13,7 @@ export declare const CSR: {
|
|
|
13
13
|
export type CSRProviderProps = ProviderProps & {
|
|
14
14
|
fonts: ReactFont[];
|
|
15
15
|
};
|
|
16
|
-
declare const CSRProvider: ({ className, appName, params, head, manifest, env, theme, prefix, children, gaTrackingId, fonts, layoutStyle, reconnect, of, }: CSRProviderProps) => import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
declare const CSRProvider: ({ className, appName, params, head, manifest, env, theme, prefix, children, gaTrackingId, fonts, layoutStyle, reconnect, wsConnect, of, }: CSRProviderProps) => import("react/jsx-runtime").JSX.Element;
|
|
17
17
|
interface CSRWrapperProps {
|
|
18
18
|
className?: string;
|
|
19
19
|
appName: string;
|
|
@@ -6,7 +6,7 @@ import { type HTMLAttributes, type ReactNode, type RefObject } from "react";
|
|
|
6
6
|
export declare const Client: {
|
|
7
7
|
(): import("react/jsx-runtime").JSX.Element;
|
|
8
8
|
Wrapper: ({ children, theme, lang, dictionary, signals, reconnect, }: ClientWrapperProps) => import("react/jsx-runtime").JSX.Element;
|
|
9
|
-
Bridge: ({ env, lang, theme, prefix, gaTrackingId }: ClientBridgeProps) => "" | import("react/jsx-runtime").JSX.Element | undefined;
|
|
9
|
+
Bridge: ({ env, lang, theme, prefix, gaTrackingId, wsConnect }: ClientBridgeProps) => "" | import("react/jsx-runtime").JSX.Element | undefined;
|
|
10
10
|
Inner: () => import("react/jsx-runtime").JSX.Element;
|
|
11
11
|
SsrBridge: ({ lang, prefix }: ClientSsrBridgeProps) => null;
|
|
12
12
|
};
|
|
@@ -36,8 +36,9 @@ interface ClientBridgeProps {
|
|
|
36
36
|
theme?: AkanTheme;
|
|
37
37
|
prefix?: string;
|
|
38
38
|
gaTrackingId?: string;
|
|
39
|
+
wsConnect?: boolean;
|
|
39
40
|
}
|
|
40
|
-
export declare const ClientBridge: ({ env, lang, theme, prefix, gaTrackingId }: ClientBridgeProps) => "" | import("react/jsx-runtime").JSX.Element | undefined;
|
|
41
|
+
export declare const ClientBridge: ({ env, lang, theme, prefix, gaTrackingId, wsConnect }: ClientBridgeProps) => "" | import("react/jsx-runtime").JSX.Element | undefined;
|
|
41
42
|
export declare const ClientInner: () => import("react/jsx-runtime").JSX.Element;
|
|
42
43
|
interface ClientSsrBridgeProps {
|
|
43
44
|
lang: string;
|
|
@@ -28,6 +28,8 @@ export interface ProviderProps {
|
|
|
28
28
|
layoutStyle?: "mobile" | "web";
|
|
29
29
|
/** Enable reconnect helper. Defaults to local operation mode in CSR. */
|
|
30
30
|
reconnect?: boolean;
|
|
31
|
+
/** Connect the client WebSocket runtime after the browser loads. */
|
|
32
|
+
wsConnect?: boolean;
|
|
31
33
|
/** Active-locale dictionary injected by the server (SSR only) to seed the client Translator. */
|
|
32
34
|
dictionary?: Record<string, Record<string, unknown>>;
|
|
33
35
|
/**
|
package/types/ui/System/SSR.d.ts
CHANGED
|
@@ -3,13 +3,13 @@ import { type ReactNode } from "react";
|
|
|
3
3
|
import { type ProviderProps } from "./Common.d.ts";
|
|
4
4
|
export declare const SSR: {
|
|
5
5
|
(): import("react/jsx-runtime").JSX.Element;
|
|
6
|
-
Provider: ({ className, appName, params, head, manifest, env, gaTrackingId, children, theme, prefix, fonts, layoutStyle, reconnect, dictionary, allDictionary, of, }: SSRProviderProps) => import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
Provider: ({ className, appName, params, head, manifest, env, gaTrackingId, children, theme, prefix, fonts, layoutStyle, reconnect, wsConnect, dictionary, allDictionary, of, }: SSRProviderProps) => import("react/jsx-runtime").JSX.Element;
|
|
7
7
|
Wrapper: ({ children, head, manifest, fonts, className, prefix, layoutStyle, }: SSRWrapperProps) => import("react/jsx-runtime").JSX.Element;
|
|
8
8
|
};
|
|
9
9
|
export type SSRProviderProps = ProviderProps & {
|
|
10
10
|
fonts?: ReactFont[];
|
|
11
11
|
};
|
|
12
|
-
declare const SSRProvider: ({ className, appName, params, head, manifest, env, gaTrackingId, children, theme, prefix, fonts, layoutStyle, reconnect, dictionary, allDictionary, of, }: SSRProviderProps) => import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
declare const SSRProvider: ({ className, appName, params, head, manifest, env, gaTrackingId, children, theme, prefix, fonts, layoutStyle, reconnect, wsConnect, dictionary, allDictionary, of, }: SSRProviderProps) => import("react/jsx-runtime").JSX.Element;
|
|
13
13
|
interface SSRWrapperProps {
|
|
14
14
|
className?: string;
|
|
15
15
|
appName: string;
|
package/ui/System/CSR.tsx
CHANGED
|
@@ -45,6 +45,7 @@ const CSRProvider = ({
|
|
|
45
45
|
fonts,
|
|
46
46
|
layoutStyle = "web",
|
|
47
47
|
reconnect = getEnv().operationMode === "local",
|
|
48
|
+
wsConnect = true,
|
|
48
49
|
of,
|
|
49
50
|
}: CSRProviderProps) => {
|
|
50
51
|
return (
|
|
@@ -72,7 +73,14 @@ const CSRProvider = ({
|
|
|
72
73
|
</Client.Wrapper>
|
|
73
74
|
<Client.Inner />
|
|
74
75
|
<CSRInner />
|
|
75
|
-
<Client.Bridge
|
|
76
|
+
<Client.Bridge
|
|
77
|
+
lang={lang}
|
|
78
|
+
env={env}
|
|
79
|
+
theme={theme}
|
|
80
|
+
prefix={prefix}
|
|
81
|
+
gaTrackingId={gaTrackingId}
|
|
82
|
+
wsConnect={wsConnect}
|
|
83
|
+
/>
|
|
76
84
|
<CSRBridge lang={lang} prefix={prefix} />
|
|
77
85
|
</>
|
|
78
86
|
)}
|
package/ui/System/Client.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
clsx,
|
|
6
6
|
Device,
|
|
7
7
|
defaultPageState,
|
|
8
|
+
fetch,
|
|
8
9
|
getPathInfo,
|
|
9
10
|
initAuth,
|
|
10
11
|
type Location,
|
|
@@ -152,9 +153,10 @@ interface ClientBridgeProps {
|
|
|
152
153
|
theme?: AkanTheme;
|
|
153
154
|
prefix?: string;
|
|
154
155
|
gaTrackingId?: string;
|
|
156
|
+
wsConnect?: boolean;
|
|
155
157
|
}
|
|
156
158
|
|
|
157
|
-
export const ClientBridge = ({ env, lang, theme, prefix, gaTrackingId }: ClientBridgeProps) => {
|
|
159
|
+
export const ClientBridge = ({ env, lang, theme, prefix, gaTrackingId, wsConnect = true }: ClientBridgeProps) => {
|
|
158
160
|
const uiOperation = st.use.uiOperation();
|
|
159
161
|
const pathname = st.use.pathname();
|
|
160
162
|
const params = st.use.params();
|
|
@@ -174,6 +176,11 @@ export const ClientBridge = ({ env, lang, theme, prefix, gaTrackingId }: ClientB
|
|
|
174
176
|
}, 2000);
|
|
175
177
|
}, []);
|
|
176
178
|
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
if (!wsConnect) return;
|
|
181
|
+
(fetch.instance as { connect: () => void }).connect();
|
|
182
|
+
}, [wsConnect]);
|
|
183
|
+
|
|
177
184
|
useEffect(() => {
|
|
178
185
|
if (getThemeCookie() !== undefined) return;
|
|
179
186
|
applyThemePolicy(theme ?? "system");
|
package/ui/System/Common.tsx
CHANGED
|
@@ -30,6 +30,8 @@ export interface ProviderProps {
|
|
|
30
30
|
layoutStyle?: "mobile" | "web";
|
|
31
31
|
/** Enable reconnect helper. Defaults to local operation mode in CSR. */
|
|
32
32
|
reconnect?: boolean;
|
|
33
|
+
/** Connect the client WebSocket runtime after the browser loads. */
|
|
34
|
+
wsConnect?: boolean;
|
|
33
35
|
/** Active-locale dictionary injected by the server (SSR only) to seed the client Translator. */
|
|
34
36
|
dictionary?: Record<string, Record<string, unknown>>;
|
|
35
37
|
/**
|
package/ui/System/SSR.tsx
CHANGED
|
@@ -29,6 +29,7 @@ const SSRProvider = ({
|
|
|
29
29
|
fonts,
|
|
30
30
|
layoutStyle = "web",
|
|
31
31
|
reconnect = getEnv().operationMode === "local",
|
|
32
|
+
wsConnect = true,
|
|
32
33
|
dictionary,
|
|
33
34
|
allDictionary,
|
|
34
35
|
of,
|
|
@@ -65,7 +66,14 @@ const SSRProvider = ({
|
|
|
65
66
|
<ClientInner />
|
|
66
67
|
</Suspense>
|
|
67
68
|
<Suspense key="client-bridge" fallback={null}>
|
|
68
|
-
<ClientBridge
|
|
69
|
+
<ClientBridge
|
|
70
|
+
key="bridge"
|
|
71
|
+
env={env}
|
|
72
|
+
theme={theme}
|
|
73
|
+
prefix={prefix}
|
|
74
|
+
gaTrackingId={gaTrackingId}
|
|
75
|
+
wsConnect={wsConnect}
|
|
76
|
+
/>
|
|
69
77
|
<ClientSsrBridge key="ssr-bridge" lang={lang} prefix={prefix} />
|
|
70
78
|
</Suspense>
|
|
71
79
|
</ClientWrapper>
|