@lytjs/ssr 6.0.0 → 6.4.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/index.cjs CHANGED
@@ -187,6 +187,38 @@ var VirtualList = component.defineComponent({
187
187
  }
188
188
  });
189
189
  var DEFAULT_CHUNK_SIZE = 4096;
190
+ var DEFAULT_TIMEOUT = 3e4;
191
+ var DEFAULT_FALLBACK_HTML = '<div id="lyt-fallback">\u670D\u52A1\u6B63\u5728\u52A0\u8F7D\u4E2D...</div>';
192
+ var StreamTimeoutError = class extends Error {
193
+ constructor(message = "Stream rendering timeout") {
194
+ super(message);
195
+ this.name = "StreamTimeoutError";
196
+ }
197
+ };
198
+ var FlowController = class {
199
+ constructor(maxBytesPerSecond) {
200
+ this.bytesSentInSecond = 0;
201
+ this.lastSecondTimestamp = Date.now();
202
+ this.maxBytesPerSecond = maxBytesPerSecond;
203
+ }
204
+ /**
205
+ * 尝试发送字节,如超出速率则等待
206
+ */
207
+ async waitForRateLimit(byteCount) {
208
+ const now = Date.now();
209
+ if (now - this.lastSecondTimestamp >= 1e3) {
210
+ this.bytesSentInSecond = 0;
211
+ this.lastSecondTimestamp = now;
212
+ }
213
+ if (this.bytesSentInSecond + byteCount > this.maxBytesPerSecond) {
214
+ const waitTime = 1e3 - (now - this.lastSecondTimestamp);
215
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
216
+ this.bytesSentInSecond = 0;
217
+ this.lastSecondTimestamp = Date.now();
218
+ }
219
+ this.bytesSentInSecond += byteCount;
220
+ }
221
+ };
190
222
  var SUSPENSE_TYPE = "Suspense";
191
223
  function isSuspenseVNode(vnode) {
192
224
  return commonIs.isObject(vnode) && (typeof vnode.type === "object" && vnode.type !== null && "__suspense" in vnode.type || commonIs.isString(vnode.type) && vnode.type === SUSPENSE_TYPE);
@@ -258,11 +290,32 @@ function renderToStream(vnode, options) {
258
290
  const {
259
291
  chunkSize = DEFAULT_CHUNK_SIZE,
260
292
  onShellReady,
261
- onError
293
+ onError,
294
+ timeout = DEFAULT_TIMEOUT,
295
+ fallbackHtml = DEFAULT_FALLBACK_HTML,
296
+ maxBytesPerSecond,
297
+ errorRecovery = true
262
298
  } = options || {};
263
299
  const encoder = new TextEncoder();
300
+ let flowController = maxBytesPerSecond ? new FlowController(maxBytesPerSecond) : null;
301
+ let timeoutId = null;
264
302
  return new ReadableStream({
265
303
  start(controller) {
304
+ timeoutId = setTimeout(() => {
305
+ try {
306
+ if (errorRecovery) {
307
+ console.warn("Stream rendering timed out, using fallback HTML");
308
+ controller.enqueue(encoder.encode(fallbackHtml));
309
+ controller.close();
310
+ onError?.(new StreamTimeoutError("Stream rendering timed out"));
311
+ } else {
312
+ controller.error(new StreamTimeoutError("Stream rendering timed out"));
313
+ }
314
+ } catch (err) {
315
+ const error = err instanceof Error ? err : new Error(String(err));
316
+ onError?.(error);
317
+ }
318
+ }, timeout);
266
319
  try {
267
320
  const suspenseBoundaryIndex = [];
268
321
  const chunks = collectChunks(vnode, suspenseBoundaryIndex);
@@ -273,29 +326,64 @@ function renderToStream(vnode, options) {
273
326
  const shellHtml = shellChunks.join("");
274
327
  const byteChunks = splitIntoByteChunks(shellHtml, chunkSize);
275
328
  for (const chunk of byteChunks) {
276
- controller.enqueue(encoder.encode(chunk));
329
+ sendChunk(controller, encoder, chunk);
277
330
  }
278
331
  onShellReady?.();
279
332
  const remainingChunks = chunks.slice(shellBoundary);
280
333
  const remainingHtml = remainingChunks.join("");
281
334
  const remainingByteChunks = splitIntoByteChunks(remainingHtml, chunkSize);
282
335
  for (const chunk of remainingByteChunks) {
283
- controller.enqueue(encoder.encode(chunk));
336
+ sendChunk(controller, encoder, chunk);
284
337
  }
285
338
  } else {
286
339
  const byteChunks = splitIntoByteChunks(fullHtml, chunkSize);
287
340
  for (const chunk of byteChunks) {
288
- controller.enqueue(encoder.encode(chunk));
341
+ sendChunk(controller, encoder, chunk);
289
342
  }
290
343
  }
344
+ if (timeoutId) {
345
+ clearTimeout(timeoutId);
346
+ timeoutId = null;
347
+ }
291
348
  controller.close();
292
349
  } catch (err) {
350
+ if (timeoutId) {
351
+ clearTimeout(timeoutId);
352
+ timeoutId = null;
353
+ }
293
354
  const error = err instanceof Error ? err : new Error(String(err));
294
355
  onError?.(error);
295
- controller.error(error);
356
+ if (errorRecovery) {
357
+ try {
358
+ console.warn("Error occurred during stream rendering, using fallback HTML");
359
+ controller.enqueue(encoder.encode(fallbackHtml));
360
+ controller.close();
361
+ } catch (e) {
362
+ controller.error(error);
363
+ }
364
+ } else {
365
+ controller.error(error);
366
+ }
367
+ }
368
+ },
369
+ cancel(reason) {
370
+ if (timeoutId) {
371
+ clearTimeout(timeoutId);
296
372
  }
373
+ console.log("Stream cancelled:", reason);
297
374
  }
298
375
  });
376
+ function sendChunk(controller, encoder2, chunk) {
377
+ const encoded = encoder2.encode(chunk);
378
+ if (flowController) {
379
+ (async () => {
380
+ await flowController.waitForRateLimit(encoded.length);
381
+ controller.enqueue(encoded);
382
+ })();
383
+ } else {
384
+ controller.enqueue(encoded);
385
+ }
386
+ }
299
387
  }
300
388
  function renderToStreamAsync(vnode, options) {
301
389
  const {
@@ -437,7 +525,12 @@ var DEFAULT_SSG_OPTIONS = {
437
525
  siteName: "LytJS Site",
438
526
  hashMode: false,
439
527
  globalScripts: [],
440
- globalStyles: []
528
+ globalStyles: [],
529
+ isr: {
530
+ revalidate: 60,
531
+ enabled: false,
532
+ fallback: false
533
+ }
441
534
  };
442
535
  function normalizePath(path) {
443
536
  let normalized = path.startsWith("/") ? path : `/${path}`;
@@ -556,6 +649,376 @@ function validatePages(pages) {
556
649
  }
557
650
  return errors;
558
651
  }
652
+ var ISRCacheManager = class {
653
+ constructor() {
654
+ this.cache = /* @__PURE__ */ new Map();
655
+ this.revalidateTasks = /* @__PURE__ */ new Map();
656
+ }
657
+ /**
658
+ * 获取缓存的页面
659
+ */
660
+ get(path) {
661
+ return this.cache.get(path);
662
+ }
663
+ /**
664
+ * 设置缓存
665
+ */
666
+ set(path, html) {
667
+ this.cache.set(path, {
668
+ html,
669
+ timestamp: Date.now(),
670
+ isRevalidating: false
671
+ });
672
+ }
673
+ /**
674
+ * 检查是否需要重新验证
675
+ */
676
+ needsRevalidation(path, revalidateSeconds) {
677
+ const entry = this.cache.get(path);
678
+ if (!entry) return true;
679
+ const age = (Date.now() - entry.timestamp) / 1e3;
680
+ return age > revalidateSeconds && !entry.isRevalidating;
681
+ }
682
+ /**
683
+ * 标记为正在重新生成
684
+ */
685
+ markRevalidating(path) {
686
+ const entry = this.cache.get(path);
687
+ if (entry) {
688
+ entry.isRevalidating = true;
689
+ }
690
+ }
691
+ /**
692
+ * 完成重新生成
693
+ */
694
+ finishRevalidation(path, html) {
695
+ this.set(path, html);
696
+ this.revalidateTasks.delete(path);
697
+ }
698
+ /**
699
+ * 获取正在进行的重新生成任务
700
+ */
701
+ getRevalidateTask(path) {
702
+ return this.revalidateTasks.get(path);
703
+ }
704
+ /**
705
+ * 设置重新生成任务
706
+ */
707
+ setRevalidateTask(path, task) {
708
+ this.revalidateTasks.set(path, task);
709
+ }
710
+ /**
711
+ * 清除过期的缓存
712
+ */
713
+ clearExpired(maxAgeSeconds) {
714
+ const now = Date.now();
715
+ let cleared = 0;
716
+ for (const [path, entry] of this.cache) {
717
+ const age = (now - entry.timestamp) / 1e3;
718
+ if (age > maxAgeSeconds) {
719
+ this.cache.delete(path);
720
+ cleared++;
721
+ }
722
+ }
723
+ return cleared;
724
+ }
725
+ /**
726
+ * 获取缓存统计信息
727
+ */
728
+ getStats() {
729
+ return {
730
+ total: this.cache.size,
731
+ paths: Array.from(this.cache.keys())
732
+ };
733
+ }
734
+ };
735
+ var isrCache = new ISRCacheManager();
736
+ function createISRMiddleware(options) {
737
+ const { staticPages, revalidate = 60, enabled = true, regenerate } = options;
738
+ for (const [path, html] of staticPages) {
739
+ isrCache.set(path, html);
740
+ }
741
+ return async (req, res, next) => {
742
+ if (!enabled) {
743
+ return next();
744
+ }
745
+ let path = req.path;
746
+ if (path.endsWith("/")) {
747
+ path = path + "index.html";
748
+ } else if (!path.endsWith(".html")) {
749
+ path = path + "/index.html";
750
+ }
751
+ const cached = isrCache.get(path);
752
+ if (cached) {
753
+ res.setHeader("Cache-Control", `s-maxage=${revalidate}, stale-while-revalidate`);
754
+ res.setHeader("X-ISR-Cache", "HIT");
755
+ res.setHeader("X-ISR-Timestamp", cached.timestamp.toString());
756
+ res.send(cached.html);
757
+ if (isrCache.needsRevalidation(path, revalidate) && regenerate) {
758
+ isrCache.markRevalidating(path);
759
+ try {
760
+ const newHtml = await regenerate(path);
761
+ isrCache.finishRevalidation(path, newHtml);
762
+ } catch (error) {
763
+ console.error("[ISR] Revalidation failed for", path, error);
764
+ const entry = isrCache.get(path);
765
+ if (entry) {
766
+ entry.isRevalidating = false;
767
+ }
768
+ }
769
+ }
770
+ } else {
771
+ if (regenerate) {
772
+ try {
773
+ const html = await regenerate(path);
774
+ isrCache.set(path, html);
775
+ res.setHeader("X-ISR-Cache", "MISS");
776
+ res.send(html);
777
+ } catch (error) {
778
+ next(error);
779
+ }
780
+ } else {
781
+ next();
782
+ }
783
+ }
784
+ };
785
+ }
786
+ async function revalidateOnDemand(path, regenerate) {
787
+ const existingTask = isrCache.getRevalidateTask(path);
788
+ if (existingTask) {
789
+ return existingTask;
790
+ }
791
+ const task = (async () => {
792
+ isrCache.markRevalidating(path);
793
+ try {
794
+ const html = await regenerate();
795
+ isrCache.finishRevalidation(path, html);
796
+ return html;
797
+ } catch (error) {
798
+ const entry = isrCache.get(path);
799
+ if (entry) {
800
+ entry.isRevalidating = false;
801
+ }
802
+ throw error;
803
+ }
804
+ })();
805
+ isrCache.setRevalidateTask(path, task);
806
+ return task;
807
+ }
808
+ function getISRCacheStats() {
809
+ return isrCache.getStats();
810
+ }
811
+ function clearISRCache(path, maxAge) {
812
+ if (path) {
813
+ const cache = isrCache.cache;
814
+ cache.delete(path);
815
+ } else if (maxAge) {
816
+ isrCache.clearExpired(maxAge);
817
+ }
818
+ }
819
+ var ServerComponentStateManager = class {
820
+ constructor() {
821
+ /** 注册的组件 */
822
+ this.registrations = /* @__PURE__ */ new Map();
823
+ /** 正在执行的预取请求 */
824
+ this.pendingPrefetches = /* @__PURE__ */ new Map();
825
+ /** 组件初始化状态 */
826
+ this.initializationStates = /* @__PURE__ */ new Map();
827
+ }
828
+ /**
829
+ * 注册服务端组件
830
+ */
831
+ register(name, registration) {
832
+ this.registrations.set(name, registration);
833
+ }
834
+ /**
835
+ * 取消注册服务端组件
836
+ */
837
+ unregister(name) {
838
+ this.registrations.delete(name);
839
+ }
840
+ /**
841
+ * 获取已注册的组件
842
+ */
843
+ getRegistration(name) {
844
+ return this.registrations.get(name);
845
+ }
846
+ /**
847
+ * 初始化服务端组件
848
+ */
849
+ async initializeComponent(name, context) {
850
+ const registration = this.registrations.get(name);
851
+ if (!registration) {
852
+ throw new Error(`Server component ${name} not registered`);
853
+ }
854
+ if (this.initializationStates.get(name)) {
855
+ return;
856
+ }
857
+ if (registration.onServerInit) {
858
+ await Promise.resolve(registration.onServerInit(context));
859
+ }
860
+ this.initializationStates.set(name, true);
861
+ }
862
+ /**
863
+ * 清理服务端组件
864
+ */
865
+ async cleanupComponent(name, context) {
866
+ const registration = this.registrations.get(name);
867
+ if (!registration) {
868
+ return;
869
+ }
870
+ if (registration.onServerCleanup) {
871
+ await Promise.resolve(registration.onServerCleanup(context));
872
+ }
873
+ this.initializationStates.delete(name);
874
+ }
875
+ /**
876
+ * 预取组件数据(带缓存)
877
+ */
878
+ async prefetchComponentData(name, context, cacheKey) {
879
+ const registration = this.registrations.get(name);
880
+ if (!registration || !registration.prefetch) {
881
+ return { data: {} };
882
+ }
883
+ const key = cacheKey || `${name}-${JSON.stringify(context)}`;
884
+ const existing = this.pendingPrefetches.get(key);
885
+ if (existing) {
886
+ return existing;
887
+ }
888
+ const promise = registration.prefetch(context).finally(() => {
889
+ this.pendingPrefetches.delete(key);
890
+ });
891
+ this.pendingPrefetches.set(key, promise);
892
+ return promise;
893
+ }
894
+ /**
895
+ * 清除所有缓存
896
+ */
897
+ clearAll() {
898
+ this.registrations.clear();
899
+ this.pendingPrefetches.clear();
900
+ this.initializationStates.clear();
901
+ }
902
+ };
903
+ var stateManager = new ServerComponentStateManager();
904
+ function registerServerComponent(name, registration) {
905
+ stateManager.register(name, registration);
906
+ }
907
+ function unregisterServerComponent(name) {
908
+ stateManager.unregister(name);
909
+ }
910
+ function collectPrefetchComponents(vnode) {
911
+ const components = [];
912
+ collectComponentsRecursive(vnode, components);
913
+ return components;
914
+ }
915
+ function collectComponentsRecursive(vnode, result) {
916
+ if (!commonIs.isObject(vnode)) {
917
+ return;
918
+ }
919
+ const node = vnode;
920
+ if (commonIs.isArray(vnode)) {
921
+ for (const child of vnode) {
922
+ collectComponentsRecursive(child, result);
923
+ }
924
+ return;
925
+ }
926
+ if (commonIs.isFunction(node.type)) {
927
+ const compName = node.type.name;
928
+ if (compName && stateManager.getRegistration(compName)) {
929
+ if (!result.includes(compName)) {
930
+ result.push(compName);
931
+ }
932
+ }
933
+ }
934
+ const children = node.children;
935
+ if (commonIs.isArray(children)) {
936
+ for (const child of children) {
937
+ collectComponentsRecursive(child, result);
938
+ }
939
+ } else if (commonIs.isObject(children)) {
940
+ collectComponentsRecursive(children, result);
941
+ }
942
+ }
943
+ async function prefetchAllComponents(components, context) {
944
+ const results = {};
945
+ const promises = [];
946
+ for (const name of components) {
947
+ const promise = stateManager.prefetchComponentData(name, context).then((result) => {
948
+ results[name] = result;
949
+ }).catch((err) => {
950
+ console.warn(`Prefetch failed for component ${name}`, err);
951
+ results[name] = { data: {} };
952
+ });
953
+ promises.push(promise);
954
+ }
955
+ await Promise.all(promises);
956
+ return results;
957
+ }
958
+ function safeSerializeState(state) {
959
+ const seen = /* @__PURE__ */ new WeakSet();
960
+ return JSON.stringify(state, (_key, value) => {
961
+ if (typeof value === "object" && value !== null) {
962
+ if (seen.has(value)) {
963
+ return "[Circular]";
964
+ }
965
+ seen.add(value);
966
+ }
967
+ if (value instanceof Date) {
968
+ return { __type: "Date", value: value.toISOString() };
969
+ }
970
+ if (value instanceof RegExp) {
971
+ return { __type: "RegExp", source: value.source, flags: value.flags };
972
+ }
973
+ if (value instanceof Set) {
974
+ return { __type: "Set", values: Array.from(value) };
975
+ }
976
+ if (value instanceof Map) {
977
+ return { __type: "Map", entries: Array.from(value.entries()) };
978
+ }
979
+ return value;
980
+ });
981
+ }
982
+ function safeDeserializeState(serialized) {
983
+ return JSON.parse(serialized, (_key, value) => {
984
+ if (typeof value === "object" && value !== null && "__type" in value) {
985
+ const typedValue = value;
986
+ switch (typedValue.__type) {
987
+ case "Date":
988
+ return new Date(typedValue.value);
989
+ case "RegExp":
990
+ return new RegExp(typedValue.source, typedValue.flags);
991
+ case "Set":
992
+ return new Set(typedValue.values);
993
+ case "Map":
994
+ return new Map(typedValue.entries);
995
+ }
996
+ }
997
+ return value;
998
+ });
999
+ }
1000
+ function buildDehydratedState(prefetchResults) {
1001
+ const state = {};
1002
+ for (const [name, result] of Object.entries(prefetchResults)) {
1003
+ state[name] = {
1004
+ componentName: name,
1005
+ props: {},
1006
+ data: result.data
1007
+ };
1008
+ }
1009
+ return state;
1010
+ }
1011
+ function ServerComponent(options) {
1012
+ return function(target) {
1013
+ registerServerComponent(options.name, {
1014
+ name: options.name,
1015
+ render: target.render || (() => ({ type: "div" })),
1016
+ prefetch: options.prefetch,
1017
+ onServerInit: options.onInit,
1018
+ onServerCleanup: options.onCleanup
1019
+ });
1020
+ };
1021
+ }
559
1022
  var HYDRATE_ATTR = "data-hydrate";
560
1023
  var HYDRATE_STRATEGY_ATTR = "data-hydrate-strategy";
561
1024
  var DEHYDRATED_STATE_ID = "__LYT_DEHYDRATED_STATE__";
@@ -721,20 +1184,33 @@ function createDehydratedState(vnode, initialState) {
721
1184
  return `<script id="${DEHYDRATED_STATE_ID}" type="application/json">${safeJson}</script>`;
722
1185
  }
723
1186
 
1187
+ exports.ServerComponent = ServerComponent;
724
1188
  exports.VirtualList = VirtualList;
1189
+ exports.buildDehydratedState = buildDehydratedState;
1190
+ exports.clearISRCache = clearISRCache;
1191
+ exports.collectPrefetchComponents = collectPrefetchComponents;
725
1192
  exports.createDehydratedState = createDehydratedState;
726
1193
  exports.createHydrationMarkers = createHydrationMarkers;
1194
+ exports.createISRMiddleware = createISRMiddleware;
727
1195
  exports.default = render_default;
728
1196
  exports.generateRouteManifest = generateRouteManifest;
729
1197
  exports.generateStaticPages = generateStaticPages;
730
1198
  exports.getHydrationStrategy = getHydrationStrategy;
1199
+ exports.getISRCacheStats = getISRCacheStats;
1200
+ exports.prefetchAllComponents = prefetchAllComponents;
1201
+ exports.registerServerComponent = registerServerComponent;
731
1202
  exports.renderToHtml = renderToHtml;
732
1203
  exports.renderToStream = renderToStream;
733
1204
  exports.renderToStreamAsync = renderToStreamAsync;
734
1205
  exports.renderToStreamEnhanced = renderToStreamEnhanced;
735
1206
  exports.renderToString = renderToString;
736
1207
  exports.resetComponentIdCounter = resetComponentIdCounter;
1208
+ exports.revalidateOnDemand = revalidateOnDemand;
1209
+ exports.safeDeserializeState = safeDeserializeState;
1210
+ exports.safeSerializeState = safeSerializeState;
737
1211
  exports.serializeHydrationState = serializeHydrationState;
1212
+ exports.stateManager = stateManager;
1213
+ exports.unregisterServerComponent = unregisterServerComponent;
738
1214
  exports.validatePages = validatePages;
739
1215
  exports.writeStaticFiles = writeStaticFiles;
740
1216
  //# sourceMappingURL=index.cjs.map