webspresso 0.0.110 → 0.0.112

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.
@@ -3,10 +3,11 @@
3
3
  /**
4
4
  * Webspresso Lightweight Client Runtime (`core/client/lite.js`)
5
5
  *
6
- * Micro-footprint (~8 KB Brotli) client runtime for server-driven applications.
6
+ * Micro-footprint (~16.9 KB Brotli) client runtime for server-driven applications.
7
7
  * Includes:
8
- * - SSR Fragment coordinator (batch refresh, invalidation, DOM morphing, focus preservation)
9
- * - Native HTML Form enhancement (`<form data-w-form>`, validation errors, double-submit protection)
8
+ * - SSR Fragment coordinator (batch refresh, invalidation, auto-revalidation, DOM morphing, focus preservation)
9
+ * - Native HTML Form enhancement (`<form data-w-form>`, validation errors, lifecycle hooks, draft persistence)
10
+ * - Shared Client Services & optional lazy UI hosts (`defineClientService`, `services.get`)
10
11
  * - Typed API client with auto-invalidation
11
12
  * Excludes:
12
13
  * - Reactive signals engine, controller proxies, and complex task coordinators.
@@ -45,6 +46,46 @@ const {
45
46
 
46
47
  const { NOOP_DEBUGGER } = require('./utils');
47
48
 
49
+ const NOOP_SERVICES = Object.freeze({
50
+ get(name) {
51
+ const err = new Error(`Unknown client service "${name}".`);
52
+ err.code = 'CLIENT_SERVICE_NOT_FOUND';
53
+ return Promise.reject(err);
54
+ },
55
+ has() {
56
+ return false;
57
+ },
58
+ isInitialized() {
59
+ return false;
60
+ },
61
+ peek() {
62
+ return undefined;
63
+ },
64
+ register() {},
65
+ registerMany() {},
66
+ dispose() {},
67
+ destroy() {},
68
+ isDisposed: false,
69
+ });
70
+
71
+ let _servicesMod = null;
72
+ function getServicesModule() {
73
+ if (!_servicesMod) {
74
+ const reqFn = typeof module !== 'undefined' && module.require ? module.require.bind(module) : require;
75
+ _servicesMod = reqFn('./services');
76
+ }
77
+ return _servicesMod;
78
+ }
79
+
80
+ let _hostMod = null;
81
+ function getHostModule() {
82
+ if (!_hostMod) {
83
+ const reqFn = typeof module !== 'undefined' && module.require ? module.require.bind(module) : require;
84
+ _hostMod = reqFn('./host');
85
+ }
86
+ return _hostMod;
87
+ }
88
+
48
89
  let _serverFragments = null;
49
90
  function getServerFragments() {
50
91
  if (!_serverFragments) {
@@ -66,6 +107,14 @@ function createLiteClientRuntime(options = {}) {
66
107
  fetch: options.fetch,
67
108
  endpoint: options.fragmentEndpoint || DEFAULT_FRAGMENT_ENDPOINT,
68
109
  debug,
110
+ staleTime: options.staleTime,
111
+ refetchOnWindowFocus: options.refetchOnWindowFocus,
112
+ refetchOnFocus: options.refetchOnFocus,
113
+ refetchOnReconnect: options.refetchOnReconnect,
114
+ refetchOnMount: options.refetchOnMount,
115
+ refetchOnVisit: options.refetchOnVisit,
116
+ refetchInterval: options.refetchInterval,
117
+ refetchIntervalInBackground: options.refetchIntervalInBackground,
69
118
  getCsrfToken: options.getCsrfToken || (() => discoverCsrfToken(rootOption)),
70
119
  });
71
120
 
@@ -80,6 +129,21 @@ function createLiteClientRuntime(options = {}) {
80
129
 
81
130
  const api = createApiClient(apiConfig);
82
131
 
132
+ let runtime = null;
133
+
134
+ const services =
135
+ options.services && typeof options.services.get === 'function' && typeof options.services.register === 'function'
136
+ ? options.services
137
+ : options.services && typeof options.services === 'object'
138
+ ? getServicesModule().createClientServiceRegistry({
139
+ root: rootOption,
140
+ api,
141
+ debug,
142
+ getRuntime: () => runtime,
143
+ services: options.services,
144
+ })
145
+ : NOOP_SERVICES;
146
+
83
147
  const formsRegistry = new Map();
84
148
  function getFormHandle(nameOrEl) {
85
149
  if (!nameOrEl) return null;
@@ -93,6 +157,7 @@ function createLiteClientRuntime(options = {}) {
93
157
  el,
94
158
  createFormHandle(el, {
95
159
  api,
160
+ services,
96
161
  onInvalidate: (tags) => fragments.invalidate(tags),
97
162
  })
98
163
  );
@@ -105,6 +170,7 @@ function createLiteClientRuntime(options = {}) {
105
170
  nameOrEl,
106
171
  createFormHandle(nameOrEl, {
107
172
  api,
173
+ services,
108
174
  onInvalidate: (tags) => fragments.invalidate(tags),
109
175
  })
110
176
  );
@@ -126,16 +192,35 @@ function createLiteClientRuntime(options = {}) {
126
192
  }
127
193
  }
128
194
 
129
- return {
195
+ function destroy() {
196
+ for (const handle of formsRegistry.values()) {
197
+ try {
198
+ handle.destroy();
199
+ } catch (_e) {}
200
+ }
201
+ formsRegistry.clear();
202
+ if (fragments && typeof fragments.dispose === 'function') {
203
+ fragments.dispose();
204
+ }
205
+ services.dispose();
206
+ }
207
+
208
+ runtime = {
130
209
  fragments,
131
210
  api,
211
+ services,
212
+ registerService: (name, def) => services.register(name, def),
132
213
  forms: {
133
214
  get: getFormHandle,
134
215
  },
135
216
  invalidate: (tags, meta) => fragments.invalidate(tags, meta),
136
217
  refresh: (idOrName, input, opts) => fragments.refresh(idOrName, input, opts),
137
218
  boot,
219
+ destroy,
220
+ dispose: destroy,
138
221
  };
222
+
223
+ return runtime;
139
224
  }
140
225
 
141
226
  /**
@@ -171,5 +256,17 @@ module.exports = {
171
256
  defaultStorage,
172
257
  StorageAdapter,
173
258
  MemoryStorage,
259
+ get defineClientService() {
260
+ return getServicesModule().defineClientService;
261
+ },
262
+ get createClientServiceRegistry() {
263
+ return getServicesModule().createClientServiceRegistry;
264
+ },
265
+ get createServiceError() {
266
+ return getServicesModule().createServiceError;
267
+ },
268
+ get createClientHostManager() {
269
+ return getHostModule().createClientHostManager;
270
+ },
174
271
  };
175
272
 
@@ -15,12 +15,13 @@
15
15
  const { z } = require('../validation');
16
16
  const { ValidationError, NotFoundError, BadRequestError, UnauthorizedError, ForbiddenError } = require('../errors');
17
17
  const { onTransactionCommit, getAmbientTransaction } = require('../orm/transaction');
18
+ const { isArray, isNumber } = require('./utils');
18
19
 
19
20
  /**
20
21
  * Execute an array of Express-style middlewares sequentially.
21
22
  */
22
23
  async function runMiddlewares(middlewares, req, res) {
23
- if (!Array.isArray(middlewares) || middlewares.length === 0) return;
24
+ if (!isArray(middlewares) || middlewares.length === 0) return;
24
25
  for (const mw of middlewares) {
25
26
  if (typeof mw !== 'function') continue;
26
27
  await new Promise((resolve, reject) => {
@@ -89,11 +90,34 @@ function defineFragment(config = {}) {
89
90
  }
90
91
  return {
91
92
  template: config.template.trim(),
92
- dependsOn: Array.isArray(config.dependsOn) ? config.dependsOn.map(String) : [],
93
+ dependsOn: isArray(config.dependsOn) ? config.dependsOn.map(String) : [],
93
94
  schema: config.schema || null,
94
95
  auth: config.auth !== undefined ? config.auth : null,
95
- middleware: Array.isArray(config.middleware) ? config.middleware : [],
96
+ middleware: isArray(config.middleware) ? config.middleware : [],
96
97
  load: typeof config.load === 'function' ? config.load : null,
98
+ staleTime: config.staleTime !== undefined ? config.staleTime : undefined,
99
+ refetchOnWindowFocus:
100
+ config.refetchOnWindowFocus !== undefined
101
+ ? config.refetchOnWindowFocus
102
+ : config.refetchOnFocus !== undefined
103
+ ? config.refetchOnFocus
104
+ : undefined,
105
+ refetchOnReconnect: config.refetchOnReconnect !== undefined ? config.refetchOnReconnect : undefined,
106
+ refetchOnMount:
107
+ config.refetchOnMount !== undefined
108
+ ? config.refetchOnMount
109
+ : config.refetchOnVisit !== undefined
110
+ ? config.refetchOnVisit
111
+ : undefined,
112
+ refetchInterval: config.refetchInterval !== undefined ? config.refetchInterval : undefined,
113
+ refetchIntervalInBackground:
114
+ config.refetchIntervalInBackground !== undefined ? config.refetchIntervalInBackground : undefined,
115
+ persistInput:
116
+ config.persistInput !== undefined
117
+ ? config.persistInput
118
+ : config.persist !== undefined
119
+ ? config.persist
120
+ : undefined,
97
121
  };
98
122
  }
99
123
 
@@ -114,7 +138,7 @@ function defineFragment(config = {}) {
114
138
  * @returns {{ __isWebspressoApiResult: true, data: T, invalidate: string[], status?: number, headers?: Record<string, string> }}
115
139
  */
116
140
  function apiResult(data, meta = {}) {
117
- const rawInvalidate = meta && Array.isArray(meta.invalidate) ? meta.invalidate : [];
141
+ const rawInvalidate = meta && isArray(meta.invalidate) ? meta.invalidate : [];
118
142
  const invalidate = Array.from(
119
143
  new Set(rawInvalidate.filter((t) => typeof t === 'string' && t.trim()).map((t) => t.trim()))
120
144
  );
@@ -122,7 +146,7 @@ function apiResult(data, meta = {}) {
122
146
  __isWebspressoApiResult: true,
123
147
  data,
124
148
  invalidate,
125
- status: meta && typeof meta.status === 'number' ? meta.status : undefined,
149
+ status: meta && isNumber(meta.status) ? meta.status : undefined,
126
150
  headers: meta && meta.headers && typeof meta.headers === 'object' ? { ...meta.headers } : undefined,
127
151
  };
128
152
  }
@@ -131,11 +155,11 @@ function apiResult(data, meta = {}) {
131
155
  * Check whether a value returned by an API handler is an `apiResult` or `{ data, invalidate }` envelope.
132
156
  */
133
157
  function isApiResultEnvelope(val) {
134
- if (!val || typeof val !== 'object' || Array.isArray(val)) return false;
158
+ if (!val || typeof val !== 'object' || isArray(val)) return false;
135
159
  if (val.__isWebspressoApiResult === true) return true;
136
160
  if (
137
161
  'data' in val &&
138
- Array.isArray(val.invalidate) &&
162
+ isArray(val.invalidate) &&
139
163
  val.invalidate.every((item) => typeof item === 'string')
140
164
  ) {
141
165
  return true;
@@ -153,7 +177,7 @@ function isApiResultEnvelope(val) {
153
177
  */
154
178
  function sendApiResult(req, res, rawResult) {
155
179
  const status = rawResult.status || (res.statusCode && res.statusCode !== 200 ? res.statusCode : 200);
156
- const rawTags = Array.isArray(rawResult.invalidate) ? rawResult.invalidate : [];
180
+ const rawTags = isArray(rawResult.invalidate) ? rawResult.invalidate : [];
157
181
  const tags = Array.from(
158
182
  new Set(rawTags.filter((t) => typeof t === 'string' && t.trim()).map((t) => t.trim()))
159
183
  );
@@ -265,7 +289,7 @@ function ensureRequestMemo(req, ctx) {
265
289
  * @returns {Record<string, unknown>}
266
290
  */
267
291
  function validateFragmentInput(fragmentName, fragmentDef, rawInput = {}) {
268
- const inputObj = rawInput && typeof rawInput === 'object' && !Array.isArray(rawInput) ? rawInput : {};
292
+ const inputObj = rawInput && typeof rawInput === 'object' && !isArray(rawInput) ? rawInput : {};
269
293
  if (!fragmentDef || !fragmentDef.schema) {
270
294
  return { ...inputObj };
271
295
  }
@@ -295,7 +319,7 @@ function validateFragmentInput(fragmentName, fragmentDef, rawInput = {}) {
295
319
  const parsed = resolvedSchema.safeParse(inputObj);
296
320
  if (!parsed.success) {
297
321
  const issues = (parsed.error && parsed.error.issues ? parsed.error.issues : []).map((iss) => ({
298
- path: Array.isArray(iss.path) ? iss.path.join('.') : String(iss.path || ''),
322
+ path: isArray(iss.path) ? iss.path.join('.') : String(iss.path || ''),
299
323
  message: iss.message,
300
324
  code: iss.code,
301
325
  }));
@@ -329,18 +353,18 @@ async function enforceFragmentAuth(fragmentName, fragmentDef, req, ctx) {
329
353
  if (!user) {
330
354
  throw new UnauthorizedError(`Authentication required to access fragment "${fragmentName}".`);
331
355
  }
332
- const roles = Array.isArray(user.roles) ? user.roles : user.role ? [user.role] : [];
356
+ const roles = isArray(user.roles) ? user.roles : user.role ? [user.role] : [];
333
357
  if (!roles.includes(authRule)) {
334
358
  throw new ForbiddenError(`Role "${authRule}" required to access fragment "${fragmentName}".`);
335
359
  }
336
360
  return;
337
361
  }
338
362
 
339
- if (Array.isArray(authRule)) {
363
+ if (isArray(authRule)) {
340
364
  if (!user) {
341
365
  throw new UnauthorizedError(`Authentication required to access fragment "${fragmentName}".`);
342
366
  }
343
- const roles = Array.isArray(user.roles) ? user.roles : user.role ? [user.role] : [];
367
+ const roles = isArray(user.roles) ? user.roles : user.role ? [user.role] : [];
344
368
  if (!authRule.some((r) => roles.includes(r))) {
345
369
  throw new ForbiddenError(`Insufficient role to access fragment "${fragmentName}".`);
346
370
  }
@@ -363,7 +387,23 @@ async function enforceFragmentAuth(fragmentName, fragmentDef, req, ctx) {
363
387
  * Notice: ONLY validated filter/pagination `input` is serialized in `data-w-fragment-input`.
364
388
  * Server loader data (`fragmentData`) is NEVER serialized as props!
365
389
  */
366
- function wrapFragmentRootHtml({ name, instanceId, pagePath, dependsOn, validatedInput, innerHtml, tag = 'div', className = '' }) {
390
+ function wrapFragmentRootHtml({
391
+ name,
392
+ instanceId,
393
+ pagePath,
394
+ dependsOn,
395
+ validatedInput,
396
+ innerHtml,
397
+ tag = 'div',
398
+ className = '',
399
+ staleTime,
400
+ refetchOnWindowFocus,
401
+ refetchOnReconnect,
402
+ refetchOnMount,
403
+ refetchInterval,
404
+ refetchIntervalInBackground,
405
+ persistInput,
406
+ }) {
367
407
  const safeTag = /^[a-zA-Z][a-zA-Z0-9-]*$/.test(tag || 'div') ? tag : 'div';
368
408
  const attrs = [
369
409
  `data-w-fragment="${escapeHtmlAttr(name)}"`,
@@ -371,7 +411,7 @@ function wrapFragmentRootHtml({ name, instanceId, pagePath, dependsOn, validated
371
411
  `data-w-fragment-page="${escapeHtmlAttr(pagePath || '/')}"`,
372
412
  ];
373
413
 
374
- if (Array.isArray(dependsOn) && dependsOn.length > 0) {
414
+ if (isArray(dependsOn) && dependsOn.length > 0) {
375
415
  attrs.push(`data-w-depends-on="${escapeHtmlAttr(dependsOn.join(','))}"`);
376
416
  }
377
417
 
@@ -379,6 +419,36 @@ function wrapFragmentRootHtml({ name, instanceId, pagePath, dependsOn, validated
379
419
  attrs.push(`data-w-fragment-input="${escapeHtmlAttr(JSON.stringify(validatedInput))}"`);
380
420
  }
381
421
 
422
+ if (staleTime !== undefined && staleTime !== null && staleTime !== false) {
423
+ attrs.push(`data-w-stale-time="${escapeHtmlAttr(String(staleTime))}"`);
424
+ }
425
+
426
+ if (refetchOnWindowFocus !== undefined && refetchOnWindowFocus !== null) {
427
+ attrs.push(`data-w-refetch-on-focus="${escapeHtmlAttr(String(refetchOnWindowFocus))}"`);
428
+ }
429
+
430
+ if (refetchOnReconnect !== undefined && refetchOnReconnect !== null) {
431
+ attrs.push(`data-w-refetch-on-reconnect="${escapeHtmlAttr(String(refetchOnReconnect))}"`);
432
+ }
433
+
434
+ if (refetchOnMount !== undefined && refetchOnMount !== null) {
435
+ attrs.push(`data-w-refetch-on-mount="${escapeHtmlAttr(String(refetchOnMount))}"`);
436
+ }
437
+
438
+ if (refetchInterval !== undefined && refetchInterval !== null && refetchInterval !== false) {
439
+ attrs.push(`data-w-refetch-interval="${escapeHtmlAttr(String(refetchInterval))}"`);
440
+ }
441
+
442
+ if (refetchIntervalInBackground !== undefined && refetchIntervalInBackground !== null) {
443
+ attrs.push(`data-w-refetch-in-background="${escapeHtmlAttr(String(refetchIntervalInBackground))}"`);
444
+ }
445
+
446
+ if (persistInput !== undefined && persistInput !== null && persistInput !== false) {
447
+ const persistAttrVal =
448
+ persistInput === true || persistInput === 'input' ? 'session' : String(persistInput);
449
+ attrs.push(`data-w-persist-input="${escapeHtmlAttr(persistAttrVal)}"`);
450
+ }
451
+
382
452
  if (className) {
383
453
  attrs.push(`class="${escapeHtmlAttr(className)}"`);
384
454
  }
@@ -432,7 +502,7 @@ async function executeAndRenderFragment({
432
502
  ensureRequestMemo(req, ctx);
433
503
 
434
504
  // 1. Run fragment-specific middlewares if any
435
- if (Array.isArray(fragmentDef.middleware) && fragmentDef.middleware.length > 0) {
505
+ if (isArray(fragmentDef.middleware) && fragmentDef.middleware.length > 0) {
436
506
  await runMiddlewares(fragmentDef.middleware, req, res);
437
507
  }
438
508
 
@@ -494,6 +564,33 @@ async function executeAndRenderFragment({
494
564
  innerHtml,
495
565
  tag: options.tag || 'div',
496
566
  className: options.class || options.className || '',
567
+ staleTime: options.staleTime !== undefined ? options.staleTime : fragmentDef.staleTime,
568
+ refetchOnWindowFocus:
569
+ options.refetchOnWindowFocus !== undefined
570
+ ? options.refetchOnWindowFocus
571
+ : options.refetchOnFocus !== undefined
572
+ ? options.refetchOnFocus
573
+ : fragmentDef.refetchOnWindowFocus,
574
+ refetchOnReconnect:
575
+ options.refetchOnReconnect !== undefined ? options.refetchOnReconnect : fragmentDef.refetchOnReconnect,
576
+ refetchOnMount:
577
+ options.refetchOnMount !== undefined
578
+ ? options.refetchOnMount
579
+ : options.refetchOnVisit !== undefined
580
+ ? options.refetchOnVisit
581
+ : fragmentDef.refetchOnMount,
582
+ refetchInterval:
583
+ options.refetchInterval !== undefined ? options.refetchInterval : fragmentDef.refetchInterval,
584
+ refetchIntervalInBackground:
585
+ options.refetchIntervalInBackground !== undefined
586
+ ? options.refetchIntervalInBackground
587
+ : fragmentDef.refetchIntervalInBackground,
588
+ persistInput:
589
+ options.persistInput !== undefined
590
+ ? options.persistInput
591
+ : options.persist !== undefined
592
+ ? options.persist
593
+ : fragmentDef.persistInput,
497
594
  });
498
595
 
499
596
  return {
@@ -542,6 +639,24 @@ function attachSsrFragmentHelper({ req, res, ctx, pageMod, pagePath, nunjucksEnv
542
639
 
543
640
  const currentRenderCtx = typeof getRenderCtx === 'function' ? getRenderCtx() : { fsy };
544
641
 
642
+ // If refreshing a parent fragment that contains this nested child fragment,
643
+ // merge the active client-side child input (`req.__fragmentChildInputs`) over static template defaults
644
+ const childInputs = req && req.__fragmentChildInputs;
645
+ let effectiveRawInput = rawInput || {};
646
+ if (childInputs && typeof childInputs === 'object') {
647
+ const override = Object.prototype.hasOwnProperty.call(childInputs, instanceId)
648
+ ? childInputs[instanceId]
649
+ : instanceId === `${cleanName}:default` && Object.prototype.hasOwnProperty.call(childInputs, cleanName)
650
+ ? childInputs[cleanName]
651
+ : undefined;
652
+ if (override && typeof override === 'object' && !isArray(override)) {
653
+ effectiveRawInput =
654
+ effectiveRawInput && typeof effectiveRawInput === 'object' && !isArray(effectiveRawInput)
655
+ ? { ...effectiveRawInput, ...override }
656
+ : { ...override };
657
+ }
658
+ }
659
+
545
660
  // Queue execution on `req.__pendingSsrFragments` and return placeholder token
546
661
  const placeholderIndex = req.__pendingSsrFragments.length;
547
662
  const token = `<!--__WEBSPRESSO_SSR_FRAGMENT_${placeholderIndex}__-->`;
@@ -551,7 +666,7 @@ function attachSsrFragmentHelper({ req, res, ctx, pageMod, pagePath, nunjucksEnv
551
666
  fragmentDef,
552
667
  instanceId,
553
668
  pagePath,
554
- rawInput: rawInput || {},
669
+ rawInput: effectiveRawInput,
555
670
  req,
556
671
  res,
557
672
  ctx,
@@ -588,7 +703,7 @@ function attachSsrFragmentHelper({ req, res, ctx, pageMod, pagePath, nunjucksEnv
588
703
  * @returns {Promise<string>}
589
704
  */
590
705
  async function resolvePendingSsrFragments(html, req) {
591
- if (typeof html !== 'string' || !req || !Array.isArray(req.__pendingSsrFragments)) {
706
+ if (typeof html !== 'string' || !req || !isArray(req.__pendingSsrFragments)) {
592
707
  return html;
593
708
  }
594
709
 
@@ -627,7 +742,7 @@ function trimTrailingSlashes(str) {
627
742
  * @returns {{ entry: object, params: Record<string, string> }|null}
628
743
  */
629
744
  function matchPageEntryByPath(pagePath, pageEntries) {
630
- if (!pagePath || !Array.isArray(pageEntries)) return null;
745
+ if (!pagePath || !isArray(pageEntries)) return null;
631
746
  const cleanPath = trimTrailingSlashes(String(pagePath).split('?')[0]) || '/';
632
747
 
633
748
  // 1. Exact static match
@@ -722,7 +837,7 @@ function createFragmentRefreshHandler(config) {
722
837
  },
723
838
  ];
724
839
  } else {
725
- if (!Array.isArray(payload.fragments) || payload.fragments.length === 0) {
840
+ if (!isArray(payload.fragments) || payload.fragments.length === 0) {
726
841
  throw new BadRequestError('Fragment refresh request requires a non-empty `fragments` array.');
727
842
  }
728
843
  requestedFragments = payload.fragments;
@@ -755,12 +870,12 @@ function createFragmentRefreshHandler(config) {
755
870
  }
756
871
 
757
872
  // 1. Execute page-level cascading middlewares (`_hooks.js`) and page route `middleware`
758
- if (Array.isArray(entry.cascadingMiddlewares) && entry.cascadingMiddlewares.length > 0) {
873
+ if (isArray(entry.cascadingMiddlewares) && entry.cascadingMiddlewares.length > 0) {
759
874
  await runMiddlewares(entry.cascadingMiddlewares, req, res);
760
875
  if (res.headersSent) return;
761
876
  }
762
877
 
763
- if (pageMod && Array.isArray(pageMod.middleware) && pageMod.middleware.length > 0) {
878
+ if (pageMod && isArray(pageMod.middleware) && pageMod.middleware.length > 0) {
764
879
  const resolvedPageMws =
765
880
  typeof resolveMiddlewareList === 'function'
766
881
  ? resolveMiddlewareList(pageMod.middleware)
@@ -779,6 +894,11 @@ function createFragmentRefreshHandler(config) {
779
894
  const ctx = typeof buildContext === 'function' ? buildContext(req, res, { fsy }) : { req, res, fsy };
780
895
  ensureRequestMemo(req, ctx);
781
896
 
897
+ req.__fragmentChildInputs =
898
+ payload.childInputs && typeof payload.childInputs === 'object' && !isArray(payload.childInputs)
899
+ ? payload.childInputs
900
+ : null;
901
+
782
902
  const renderedInstanceIds = new Set();
783
903
  attachSsrFragmentHelper({
784
904
  req,