inertia-sails 1.3.2 → 1.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.
Files changed (41) hide show
  1. package/README.md +55 -2
  2. package/index.js +133 -81
  3. package/lib/handle-bad-request.js +20 -6
  4. package/lib/helpers/build-page-object.js +117 -12
  5. package/lib/helpers/inertia-headers.js +4 -1
  6. package/lib/helpers/is-inertia-partial-request.js +10 -0
  7. package/lib/helpers/is-inertia-request.js +9 -0
  8. package/lib/helpers/request-context.js +49 -8
  9. package/lib/helpers/resolve-asset-version.js +12 -0
  10. package/lib/helpers/resolve-validation-errors.js +12 -5
  11. package/lib/location.js +11 -0
  12. package/lib/middleware/inertia-middleware.js +7 -2
  13. package/lib/props/always-prop.js +6 -2
  14. package/lib/props/defer-prop.js +46 -6
  15. package/lib/props/get-partial-data.js +9 -0
  16. package/lib/props/merge-prop.js +7 -4
  17. package/lib/props/merge-targets.js +114 -0
  18. package/lib/props/mergeable-prop.js +87 -0
  19. package/lib/props/once-prop.js +9 -1
  20. package/lib/props/optional-prop.js +12 -4
  21. package/lib/props/pick-props-to-resolve.js +14 -1
  22. package/lib/props/resolve-deferred-props.js +17 -1
  23. package/lib/props/resolve-except-props.js +11 -0
  24. package/lib/props/resolve-merge-props.js +81 -20
  25. package/lib/props/resolve-once-props.js +17 -7
  26. package/lib/props/resolve-only-props.js +10 -3
  27. package/lib/props/resolve-page-props.js +72 -13
  28. package/lib/props/resolve-scroll-props.js +17 -3
  29. package/lib/props/scroll-prop.js +30 -8
  30. package/lib/render.js +68 -9
  31. package/lib/responses/server-error.js +23 -6
  32. package/lib/types.js +68 -0
  33. package/package.json +3 -2
  34. package/test.js +53 -12
  35. package/tests/helpers/build-page-object.test.js +183 -0
  36. package/tests/helpers/preserve-fragment.test.js +97 -0
  37. package/tests/props/merge-targets.test.js +74 -0
  38. package/tests/props/resolve-merge-props.test.js +151 -0
  39. package/tests/props/resolve-page-props.test.js +70 -0
  40. package/tests/props/resolve-scroll-props.test.js +51 -0
  41. package/tests/render.test.js +197 -0
@@ -4,23 +4,103 @@ const resolveMergeProps = require('../props/resolve-merge-props')
4
4
  const { resolveOncePropsMetadata } = require('../props/resolve-once-props')
5
5
  const resolvePageProps = require('../props/resolve-page-props')
6
6
  const resolveScrollProps = require('../props/resolve-scroll-props')
7
+ const resolveAssetVersion = require('./resolve-asset-version')
8
+
9
+ /**
10
+ * @typedef {Object} InertiaHookApi
11
+ * @property {() => Object.<string, *>} getShared
12
+ * @property {() => boolean} shouldClearHistory
13
+ * @property {() => boolean} shouldEncryptHistory
14
+ * @property {(req: BuildPageObjectRequest) => boolean} consumePreserveFragment
15
+ * @property {(req: BuildPageObjectRequest) => Object.<string, *>} consumeFlash
16
+ */
17
+
18
+ /**
19
+ * @typedef {Object} SailsLike
20
+ * @property {Object.<string, *>} config
21
+ * @property {InertiaHookApi} inertia
22
+ */
23
+
24
+ /**
25
+ * @typedef {Object} BuildPageObjectRequest
26
+ * @property {string} [url]
27
+ * @property {string} [originalUrl]
28
+ * @property {SailsLike} _sails
29
+ * @property {(header: string) => string|undefined} get
30
+ */
7
31
 
8
32
  /**
9
33
  * @typedef {Object} InertiaPageObject
10
34
  * @property {string} component - The component name to render
11
35
  * @property {string} url - The current URL
12
- * @property {string|number} version - Asset version for cache busting
36
+ * @property {string|number|null} version - Asset version for cache busting
13
37
  * @property {Object.<string, *>} props - Resolved page props
14
- * @property {boolean} clearHistory - Whether to clear browser history
15
- * @property {boolean} encryptHistory - Whether to encrypt history state
38
+ * @property {boolean} [clearHistory] - Whether to clear browser history
39
+ * @property {boolean} [encryptHistory] - Whether to encrypt history state
40
+ * @property {boolean} [preserveFragment] - Whether to preserve URL fragments across redirects
41
+ * @property {string[]} [sharedProps] - Shared prop keys included in this response
16
42
  * @property {string[]} [mergeProps] - Props that should be merged on client
43
+ * @property {string[]} [prependProps] - Props that should be prepended on client
17
44
  * @property {string[]} [deepMergeProps] - Props that should be deep merged
45
+ * @property {string[]} [matchPropsOn] - Prop paths to use for matching merge items
18
46
  * @property {Object.<string, string[]>} [deferredProps] - Deferred props by group
47
+ * @property {string[]} [rescuedProps] - Deferred props rescued after callback failures
19
48
  * @property {Object.<string, *>} [onceProps] - Once-prop metadata
20
49
  * @property {Object.<string, *>} [scrollProps] - Scroll props for InfiniteScroll component
21
50
  * @property {Object.<string, *>} [flash] - Flash data (not persisted in history)
22
51
  */
23
52
 
53
+ /**
54
+ * @typedef {'mergeProps'|'prependProps'|'deepMergeProps'|'matchPropsOn'} PathMetadataKey
55
+ */
56
+
57
+ /**
58
+ * @param {InertiaPageObject} page
59
+ * @param {PathMetadataKey} key
60
+ */
61
+ function removeEmptyArrayMetadata(page, key) {
62
+ if (Array.isArray(page[key]) && page[key].length === 0) {
63
+ delete page[key]
64
+ }
65
+ }
66
+
67
+ /**
68
+ * @param {InertiaPageObject} page
69
+ * @param {string[]} rescuedProps
70
+ */
71
+ function removeRescuedPropMetadata(page, rescuedProps) {
72
+ if (rescuedProps.length === 0) return
73
+
74
+ const rescuedRootProps = new Set(rescuedProps)
75
+ /** @param {string} path */
76
+ const isNotRescuedPath = (path) => !rescuedRootProps.has(path.split('.')[0])
77
+ /** @type {PathMetadataKey[]} */
78
+ const pathMetadataKeys = [
79
+ 'mergeProps',
80
+ 'prependProps',
81
+ 'deepMergeProps',
82
+ 'matchPropsOn'
83
+ ]
84
+
85
+ pathMetadataKeys.forEach((key) => {
86
+ if (Array.isArray(page[key])) {
87
+ page[key] = page[key].filter(isNotRescuedPath)
88
+ removeEmptyArrayMetadata(page, key)
89
+ }
90
+ })
91
+
92
+ const scrollProps = page.scrollProps
93
+ if (scrollProps) {
94
+ rescuedProps.forEach((key) => {
95
+ delete scrollProps[key]
96
+ })
97
+
98
+ if (Object.keys(scrollProps).length === 0) {
99
+ delete page.scrollProps
100
+ }
101
+ }
102
+ }
103
+
24
104
  /**
25
105
  * Build the Inertia page object for a response.
26
106
  *
@@ -30,42 +110,67 @@ const resolveScrollProps = require('../props/resolve-scroll-props')
30
110
  * Uses request-scoped shared props (via AsyncLocalStorage) merged with
31
111
  * global shared props to prevent data leaking between concurrent requests.
32
112
  *
33
- * @param {Object} req - Express/Sails request object
113
+ * @param {BuildPageObjectRequest} req - Express/Sails request object
34
114
  * @param {string} component - The component name to render
35
115
  * @param {Object.<string, *>} pageProps - Props specific to this page
36
116
  * @returns {Promise<InertiaPageObject>} - The complete page object
37
117
  */
38
118
  module.exports = async function buildPageObject(req, component, pageProps) {
39
119
  const sails = req._sails
40
- let url = req.url || req.originalUrl
41
- const assetVersion = sails.config.inertia.version
42
- const currentVersion =
43
- typeof assetVersion === 'function' ? assetVersion() : assetVersion
120
+ let url = req.url || req.originalUrl || '/'
121
+ const currentVersion = resolveAssetVersion(sails)
122
+
123
+ const sharedProps = sails.inertia.getShared()
124
+ const sharedPropKeys = Object.keys(sharedProps)
44
125
 
45
126
  // Merge props: global shared → request-scoped shared → page-specific
46
127
  // This ensures user-specific data (from share()) doesn't leak between requests
47
128
  const allProps = {
48
- ...sails.inertia.getShared(), // Merges global + request-scoped
129
+ ...sharedProps, // Merges global + request-scoped
49
130
  ...pageProps
50
131
  }
51
132
 
52
133
  const propsToResolve = pickPropsToResolve(req, component, allProps)
134
+ const clearHistory = sails.inertia.shouldClearHistory()
135
+ const encryptHistory = sails.inertia.shouldEncryptHistory()
136
+ const preserveFragment = sails.inertia.consumePreserveFragment(req)
137
+ const resolvedPageProps = await resolvePageProps.withMetadata(propsToResolve)
53
138
 
54
139
  // Build the page object with all metadata
55
140
  // Use request-scoped history settings (prevents race conditions)
141
+ /** @type {InertiaPageObject} */
56
142
  const page = {
57
143
  component,
58
144
  url,
59
145
  version: currentVersion,
60
- props: await resolvePageProps(propsToResolve),
61
- clearHistory: sails.inertia.shouldClearHistory(),
62
- encryptHistory: sails.inertia.shouldEncryptHistory(),
146
+ props: resolvedPageProps.props,
63
147
  ...resolveMergeProps(req, allProps),
64
148
  ...resolveDeferredProps(req, component, allProps),
65
149
  ...resolveOncePropsMetadata(allProps),
66
150
  ...resolveScrollProps(allProps)
67
151
  }
68
152
 
153
+ if (clearHistory) {
154
+ page.clearHistory = true
155
+ }
156
+
157
+ if (encryptHistory) {
158
+ page.encryptHistory = true
159
+ }
160
+
161
+ if (preserveFragment) {
162
+ page.preserveFragment = true
163
+ }
164
+
165
+ if (resolvedPageProps.rescuedProps.length > 0) {
166
+ page.rescuedProps = resolvedPageProps.rescuedProps
167
+ removeRescuedPropMetadata(page, resolvedPageProps.rescuedProps)
168
+ }
169
+
170
+ if (sharedPropKeys.length > 0) {
171
+ page.sharedProps = sharedPropKeys
172
+ }
173
+
69
174
  // Consume flash data from session and add to props
70
175
  // Flash data is included in props.flash so it's accessible via usePage().props.flash
71
176
  // Note: Unlike regular props, flash data should NOT be persisted in browser history
@@ -23,6 +23,8 @@ const PARTIAL_COMPONENT = 'X-Inertia-Partial-Component'
23
23
  const LOCATION = 'X-Inertia-Location'
24
24
  /** @type {string} - Comma-separated list of once-props client already has */
25
25
  const EXCEPT_ONCE_PROPS = 'X-Inertia-Except-Once-Props'
26
+ /** @type {string} - InfiniteScroll merge direction */
27
+ const INFINITE_SCROLL_MERGE_INTENT = 'X-Inertia-Infinite-Scroll-Merge-Intent'
26
28
 
27
29
  module.exports = {
28
30
  INERTIA,
@@ -33,5 +35,6 @@ module.exports = {
33
35
  RESET,
34
36
  PARTIAL_COMPONENT,
35
37
  LOCATION,
36
- EXCEPT_ONCE_PROPS
38
+ EXCEPT_ONCE_PROPS,
39
+ INFINITE_SCROLL_MERGE_INTENT
37
40
  }
@@ -1,4 +1,14 @@
1
1
  const { PARTIAL_COMPONENT } = require('./inertia-headers')
2
+
3
+ /**
4
+ * @typedef {import('../types').InertiaRequest} InertiaRequest
5
+ */
6
+
7
+ /**
8
+ * @param {InertiaRequest} req
9
+ * @param {string} component
10
+ * @returns {boolean}
11
+ */
2
12
  module.exports = function isInertiaPartialRequest(req, component) {
3
13
  return req.get(PARTIAL_COMPONENT) === component
4
14
  }
@@ -1,4 +1,13 @@
1
1
  const { INERTIA } = require('./inertia-headers')
2
+
3
+ /**
4
+ * @typedef {import('../types').InertiaRequest} InertiaRequest
5
+ */
6
+
7
+ /**
8
+ * @param {InertiaRequest} req
9
+ * @returns {any}
10
+ */
2
11
  module.exports = function isInertiaRequest(req) {
3
12
  return req.get(INERTIA)
4
13
  }
@@ -14,12 +14,31 @@
14
14
  * sharedLocals: {}, // Request-scoped locals for root EJS template
15
15
  * encryptHistory: null, // Request-scoped history encryption (null = use default)
16
16
  * clearHistory: false, // Request-scoped clear history flag
17
+ * preserveFragment: false, // Request-scoped fragment preservation flag
17
18
  * refreshOnceProps: [], // Props to force-refresh for this request
18
19
  * rootView: null // Request-scoped root view template (null = use default)
19
20
  * }
20
21
  */
21
22
  const { AsyncLocalStorage } = require('async_hooks')
22
23
 
24
+ /**
25
+ * @typedef {import('../types').InertiaRequest} InertiaRequest
26
+ * @typedef {import('../types').InertiaResponse} InertiaResponse
27
+ * @typedef {import('../types').InertiaProps} InertiaProps
28
+ *
29
+ * @typedef {Object} RequestContext
30
+ * @property {InertiaRequest} req
31
+ * @property {InertiaResponse} res
32
+ * @property {InertiaProps} sharedProps
33
+ * @property {InertiaProps} sharedLocals
34
+ * @property {boolean|null} encryptHistory
35
+ * @property {boolean} clearHistory
36
+ * @property {boolean} preserveFragment
37
+ * @property {string[]} refreshOnceProps
38
+ * @property {string|null} rootView
39
+ */
40
+
41
+ /** @type {AsyncLocalStorage<RequestContext>} */
23
42
  const requestContext = new AsyncLocalStorage()
24
43
 
25
44
  module.exports = {
@@ -30,12 +49,13 @@ module.exports = {
30
49
 
31
50
  /**
32
51
  * Run a callback with request context stored
33
- * @param {Object} req - The request object
34
- * @param {Object} res - The response object
35
- * @param {Function} callback - The callback to run
52
+ * @param {InertiaRequest} req - The request object
53
+ * @param {InertiaResponse} res - The response object
54
+ * @param {() => any} callback - The callback to run
36
55
  * @returns {*} - The result of the callback
37
56
  */
38
57
  run(req, res, callback) {
58
+ /** @type {RequestContext} */
39
59
  const context = {
40
60
  req,
41
61
  res,
@@ -43,6 +63,7 @@ module.exports = {
43
63
  sharedLocals: {},
44
64
  encryptHistory: null,
45
65
  clearHistory: false,
66
+ preserveFragment: false,
46
67
  refreshOnceProps: [], // Props to force-refresh for this request
47
68
  rootView: null // Request-scoped root view template
48
69
  }
@@ -51,7 +72,7 @@ module.exports = {
51
72
 
52
73
  /**
53
74
  * Get the full context object
54
- * @returns {Object|undefined} - The current context or undefined if not in context
75
+ * @returns {RequestContext|undefined} - The current context or undefined if not in context
55
76
  */
56
77
  getContext() {
57
78
  return requestContext.getStore()
@@ -59,7 +80,7 @@ module.exports = {
59
80
 
60
81
  /**
61
82
  * Get the current request from context
62
- * @returns {Object|undefined} - The current request or undefined if not in context
83
+ * @returns {InertiaRequest|undefined} - The current request or undefined if not in context
63
84
  */
64
85
  getRequest() {
65
86
  const context = requestContext.getStore()
@@ -68,7 +89,7 @@ module.exports = {
68
89
 
69
90
  /**
70
91
  * Get the current response from context
71
- * @returns {Object|undefined} - The current response or undefined if not in context
92
+ * @returns {InertiaResponse|undefined} - The current response or undefined if not in context
72
93
  */
73
94
  getResponse() {
74
95
  const context = requestContext.getStore()
@@ -77,7 +98,7 @@ module.exports = {
77
98
 
78
99
  /**
79
100
  * Get request-scoped shared props
80
- * @returns {Object} - The shared props for this request
101
+ * @returns {InertiaProps} - The shared props for this request
81
102
  */
82
103
  getSharedProps() {
83
104
  const context = requestContext.getStore()
@@ -98,7 +119,7 @@ module.exports = {
98
119
 
99
120
  /**
100
121
  * Get request-scoped shared locals
101
- * @returns {Object} - The shared locals for this request
122
+ * @returns {InertiaProps} - The shared locals for this request
102
123
  */
103
124
  getSharedLocals() {
104
125
  const context = requestContext.getStore()
@@ -157,6 +178,26 @@ module.exports = {
157
178
  }
158
179
  },
159
180
 
181
+ /**
182
+ * Get request-scoped preserve fragment flag
183
+ * @returns {boolean} - Whether to preserve the URL fragment
184
+ */
185
+ getPreserveFragment() {
186
+ const context = requestContext.getStore()
187
+ return context?.preserveFragment || false
188
+ },
189
+
190
+ /**
191
+ * Set request-scoped preserve fragment flag
192
+ * @param {boolean} preserve - Whether to preserve the URL fragment
193
+ */
194
+ setPreserveFragment(preserve) {
195
+ const context = requestContext.getStore()
196
+ if (context) {
197
+ context.preserveFragment = preserve
198
+ }
199
+ },
200
+
160
201
  /**
161
202
  * Get the list of once props to force-refresh
162
203
  * @returns {string[]} - Array of prop keys to refresh
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @typedef {import('../types').SailsLike} SailsLike
3
+ */
4
+
5
+ /**
6
+ * @param {SailsLike} sails
7
+ * @returns {any}
8
+ */
9
+ module.exports = function resolveAssetVersion(sails) {
10
+ const assetVersion = sails.config.inertia.version
11
+ return typeof assetVersion === 'function' ? assetVersion() : assetVersion
12
+ }
@@ -1,29 +1,36 @@
1
- // @ts-nocheck
2
1
  const { ERROR_BAG } = require('./inertia-headers')
3
2
 
3
+ /**
4
+ * @typedef {import('../types').InertiaRequest} InertiaRequest
5
+ * @typedef {Record<string, string|string[]>} ValidationErrors
6
+ */
7
+
4
8
  /**
5
9
  * @module resolveValidationErrors
6
10
  * @description Resolves and formats validation errors from the session for Inertia responses.
7
11
  *
8
- * @param {Object} req - The current HTTP request object.
9
- * @returns {Object} An object representing the validation errors in the desired format.
12
+ * @param {InertiaRequest} req - The current HTTP request object.
13
+ * @returns {ValidationErrors} An object representing the validation errors in the desired format.
10
14
  */
11
15
  module.exports = function resolveValidationErrors(req) {
12
16
  if (!req.session || !req.session.errors) {
13
17
  return {}
14
18
  }
15
19
 
16
- const flashedErrors = req.session.errors
20
+ const flashedErrors = /** @type {Record<string, string[]>} */ (
21
+ req.session.errors
22
+ )
17
23
 
18
24
  const collectedErrors = Object.keys(flashedErrors).reduce((result, bag) => {
19
25
  const errorsForBag = flashedErrors[bag]
26
+ /** @type {string[]} */
20
27
  const mappedErrors = errorsForBag.map((error) =>
21
28
  error.replace(/"([^"]+)"/, '$1')
22
29
  )
23
30
  // Ensure that single errors are wrapped in an array
24
31
  result[bag] = mappedErrors.length > 1 ? mappedErrors : mappedErrors[0]
25
32
  return result
26
- }, {})
33
+ }, /** @type {ValidationErrors} */ ({}))
27
34
 
28
35
  const inertiaErrorBag = req.headers[ERROR_BAG]
29
36
 
package/lib/location.js CHANGED
@@ -1,5 +1,16 @@
1
1
  const { INERTIA, LOCATION } = require('./helpers/inertia-headers')
2
2
 
3
+ /**
4
+ * @typedef {import('./types').InertiaRequest} InertiaRequest
5
+ * @typedef {import('./types').InertiaResponse} InertiaResponse
6
+ */
7
+
8
+ /**
9
+ * @param {InertiaRequest} req
10
+ * @param {InertiaResponse} res
11
+ * @param {string} url
12
+ * @returns {any}
13
+ */
3
14
  module.exports = function (req, res, url) {
4
15
  if (req.get(INERTIA)) {
5
16
  // If the method is PUT, PATCH, or DELETE, force a 303 redirect (so the next request is a GET)
@@ -1,6 +1,11 @@
1
1
  const resolveValidationErrors = require('../helpers/resolve-validation-errors')
2
2
  const requestContext = require('../helpers/request-context')
3
3
 
4
+ /**
5
+ * @typedef {import('../types').InertiaRequest} InertiaRequest
6
+ * @typedef {import('../types').InertiaResponse} InertiaResponse
7
+ */
8
+
4
9
  /**
5
10
  * Inertia middleware that handles validation errors.
6
11
  *
@@ -11,8 +16,8 @@ const requestContext = require('../helpers/request-context')
11
16
  * This middleware handles:
12
17
  * - Validation errors from redirects (shared as 'errors' prop)
13
18
  *
14
- * @param {Object} hook - The inertia-sails hook instance
15
- * @returns {Function} Express/Sails middleware function
19
+ * @param {Record<string, any>} hook - The inertia-sails hook instance
20
+ * @returns {(req: InertiaRequest, res: InertiaResponse, next: () => any) => any} Express/Sails middleware function
16
21
  */
17
22
  function inertia(hook) {
18
23
  return function inertiaMiddleware(req, res, next) {
@@ -1,5 +1,9 @@
1
1
  const MergeableProp = require('./mergeable-prop')
2
2
 
3
+ /**
4
+ * @typedef {import('../types').PropCallback} PropCallback
5
+ */
6
+
3
7
  /**
4
8
  * AlwaysProp - A prop that is always resolved, even during partial reloads.
5
9
  *
@@ -22,11 +26,11 @@ const MergeableProp = require('./mergeable-prop')
22
26
  module.exports = class AlwaysProp extends MergeableProp {
23
27
  /**
24
28
  * Create a new AlwaysProp instance
25
- * @param {Function} callback - The callback function to resolve the prop value
29
+ * @param {PropCallback} callback - The callback function to resolve the prop value
26
30
  */
27
31
  constructor(callback) {
28
32
  super()
29
- /** @type {Function} */
33
+ /** @type {PropCallback} */
30
34
  this.callback = callback
31
35
  }
32
36
  }
@@ -1,6 +1,13 @@
1
1
  const ignoreFirstLoadSymbol = require('../helpers/ignore-first-load-symbol')
2
2
  const MergeableProp = require('./mergeable-prop')
3
3
 
4
+ /**
5
+ * @typedef {import('../types').PropCallback} PropCallback
6
+ *
7
+ * @typedef {Object} DeferPropOptions
8
+ * @property {boolean} [rescue=false] - Whether callback failures should be rescued
9
+ */
10
+
4
11
  /**
5
12
  * DeferProp - A prop that loads after the initial page render.
6
13
  *
@@ -30,17 +37,30 @@ const MergeableProp = require('./mergeable-prop')
30
37
  module.exports = class DeferProp extends MergeableProp {
31
38
  /**
32
39
  * Create a new DeferProp instance
33
- * @param {Function} callback - The callback function to resolve the prop value
34
- * @param {string} [group='default'] - The group name for loading props together
40
+ * @param {PropCallback} callback - The callback function to resolve the prop value
41
+ * @param {string|DeferPropOptions} [group='default'] - The group name for loading props together, or options
42
+ * @param {DeferPropOptions} [options] - Deferred prop options
35
43
  */
36
- constructor(callback, group) {
44
+ constructor(callback, group = 'default', options = {}) {
37
45
  super()
38
- /** @type {Function} */
46
+ if (typeof group === 'object' && group !== null) {
47
+ options = group
48
+ group = 'default'
49
+ }
50
+ const groupName = typeof group === 'string' ? group : 'default'
51
+
52
+ /** @type {PropCallback} */
39
53
  this.callback = callback
40
54
  /** @type {string} */
41
- this.group = group
55
+ this.group = groupName
42
56
  /** @type {boolean} */
43
- this[ignoreFirstLoadSymbol] = true
57
+ this.shouldRescue = options.rescue === true
58
+ Object.defineProperty(this, ignoreFirstLoadSymbol, {
59
+ value: true,
60
+ enumerable: true,
61
+ configurable: true,
62
+ writable: true
63
+ })
44
64
  }
45
65
 
46
66
  /**
@@ -50,4 +70,24 @@ module.exports = class DeferProp extends MergeableProp {
50
70
  getGroup() {
51
71
  return this.group
52
72
  }
73
+
74
+ /**
75
+ * Rescue callback failures instead of failing the whole deferred response.
76
+ * The failed prop is omitted from props and reported in rescuedProps.
77
+ *
78
+ * @param {boolean} rescue - Whether callback failures should be rescued
79
+ * @returns {DeferProp} - Returns this for chaining
80
+ */
81
+ rescue(rescue = true) {
82
+ this.shouldRescue = rescue
83
+ return this
84
+ }
85
+
86
+ /**
87
+ * Get whether this deferred prop should rescue callback failures.
88
+ * @returns {boolean} - Whether callback failures should be rescued
89
+ */
90
+ shouldRescueProp() {
91
+ return this.shouldRescue
92
+ }
53
93
  }
@@ -1,3 +1,12 @@
1
+ /**
2
+ * @typedef {import('../types').InertiaProps} InertiaProps
3
+ */
4
+
5
+ /**
6
+ * @param {InertiaProps} props
7
+ * @param {string[]} [only]
8
+ * @returns {InertiaProps}
9
+ */
1
10
  module.exports = function getPartialData(props, only = []) {
2
11
  return Object.assign({}, ...only.map((key) => ({ [key]: props[key] })))
3
12
  }
@@ -1,5 +1,9 @@
1
1
  const MergeableProp = require('./mergeable-prop')
2
2
 
3
+ /**
4
+ * @typedef {import('../types').PropCallback} PropCallback
5
+ */
6
+
3
7
  /**
4
8
  * MergeProp - A prop that merges with existing data during partial reloads.
5
9
  *
@@ -22,14 +26,13 @@ const MergeableProp = require('./mergeable-prop')
22
26
  module.exports = class MergeProp extends MergeableProp {
23
27
  /**
24
28
  * Create a new MergeProp instance
25
- * @param {Function} callback - The callback function to resolve the prop value
29
+ * @param {PropCallback} callback - The callback function to resolve the prop value
26
30
  */
27
31
  constructor(callback) {
28
32
  super()
29
- /** @type {Function} */
33
+ /** @type {PropCallback} */
30
34
  this.callback = callback
31
- /** @type {boolean} */
32
- this.shouldMerge = true
35
+ this.merge()
33
36
  }
34
37
 
35
38
  /**