@sanity/sdk 3.0.0-rc.0 → 3.0.0-rc.1

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 (197) hide show
  1. package/dist/_chunks-dts/createGroqSearchFilter.d.ts +1147 -0
  2. package/dist/_chunks-dts/createGroqSearchFilter.d.ts.map +1 -0
  3. package/dist/_chunks-es/_internal.js +134 -0
  4. package/dist/_chunks-es/_internal.js.map +1 -0
  5. package/dist/_chunks-es/createGroqSearchFilter.js +1864 -0
  6. package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -0
  7. package/dist/_chunks-es/telemetryManager.js +93 -0
  8. package/dist/_chunks-es/telemetryManager.js.map +1 -0
  9. package/dist/_exports/_internal.d.ts +95 -0
  10. package/dist/_exports/_internal.d.ts.map +1 -0
  11. package/dist/_exports/_internal.js +24 -0
  12. package/dist/_exports/_internal.js.map +1 -0
  13. package/dist/index.d.ts +1079 -1076
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +1967 -2302
  16. package/dist/index.js.map +1 -1
  17. package/package.json +48 -47
  18. package/src/_exports/_internal.ts +26 -0
  19. package/src/_exports/index.ts +120 -11
  20. package/src/access/checkPermissions.test.ts +55 -0
  21. package/src/access/checkPermissions.ts +54 -0
  22. package/src/agent/agentActions.test.ts +16 -60
  23. package/src/agent/agentActions.ts +25 -38
  24. package/src/applications/applications.test-d.ts +83 -0
  25. package/src/applications/applications.test.ts +130 -0
  26. package/src/applications/applications.ts +364 -0
  27. package/src/applications/userApplications.test.ts +126 -0
  28. package/src/applications/userApplications.ts +261 -0
  29. package/src/auth/authLogger.ts +30 -0
  30. package/src/auth/authMode.test.ts +25 -0
  31. package/src/auth/authMode.ts +6 -3
  32. package/src/auth/authStore.test.ts +194 -13
  33. package/src/auth/authStore.ts +59 -26
  34. package/src/auth/getOrganizationVerificationState.test.ts +18 -17
  35. package/src/auth/getOrganizationVerificationState.ts +4 -4
  36. package/src/auth/handleAuthCallback.test.ts +35 -1
  37. package/src/auth/handleAuthCallback.ts +25 -6
  38. package/src/auth/logout.test.ts +75 -2
  39. package/src/auth/logout.ts +23 -4
  40. package/src/auth/refreshStampedToken.test.ts +219 -469
  41. package/src/auth/refreshStampedToken.ts +79 -195
  42. package/src/auth/studioAuth.ts +1 -1
  43. package/src/auth/studioModeAuth.ts +2 -1
  44. package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +146 -13
  45. package/src/auth/subscribeToStateAndFetchCurrentUser.ts +37 -6
  46. package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +2 -0
  47. package/src/client/clientStore.test.ts +62 -37
  48. package/src/client/clientStore.ts +45 -75
  49. package/src/client/liveEvents.test.ts +151 -0
  50. package/src/client/liveEvents.ts +107 -0
  51. package/src/comlink/controller/actions/destroyController.test.ts +4 -1
  52. package/src/comlink/controller/actions/getOrCreateChannel.test.ts +4 -1
  53. package/src/comlink/controller/actions/getOrCreateChannel.ts +2 -2
  54. package/src/comlink/controller/actions/getOrCreateController.test.ts +4 -1
  55. package/src/comlink/controller/actions/releaseChannel.test.ts +1 -1
  56. package/src/comlink/controller/comlinkControllerStore.test.ts +4 -1
  57. package/src/comlink/node/actions/getOrCreateNode.test.ts +9 -3
  58. package/src/comlink/node/actions/getOrCreateNode.ts +2 -2
  59. package/src/comlink/node/actions/releaseNode.test.ts +7 -4
  60. package/src/comlink/node/comlinkNodeStore.test.ts +2 -2
  61. package/src/comlink/node/getNodeState.test.ts +1 -1
  62. package/src/comlink/node/getNodeState.ts +2 -1
  63. package/src/config/__tests__/handles.test.ts +18 -12
  64. package/src/config/handles.ts +25 -7
  65. package/src/config/sanityConfig.ts +109 -79
  66. package/src/datasets/datasets.test.ts +2 -2
  67. package/src/datasets/datasets.ts +10 -4
  68. package/src/document/actions.test.ts +115 -33
  69. package/src/document/actions.ts +212 -15
  70. package/src/document/applyDocumentActions.test.ts +38 -12
  71. package/src/document/applyDocumentActions.ts +30 -9
  72. package/src/document/documentConstants.ts +7 -0
  73. package/src/document/documentStore.concurrency.test.ts +1306 -0
  74. package/src/document/documentStore.test.ts +673 -278
  75. package/src/document/documentStore.ts +162 -71
  76. package/src/document/events.test.ts +57 -2
  77. package/src/document/events.ts +82 -26
  78. package/src/document/listen.test.ts +56 -0
  79. package/src/document/listen.ts +74 -21
  80. package/src/document/listenerEventOperators.test.ts +311 -0
  81. package/src/document/listenerEventOperators.ts +217 -0
  82. package/src/document/patchOperations.test.ts +49 -2
  83. package/src/document/patchOperations.ts +50 -0
  84. package/src/document/permissions.test.ts +105 -60
  85. package/src/document/permissions.ts +44 -19
  86. package/src/document/processActions/create.ts +138 -0
  87. package/src/document/processActions/delete.ts +100 -0
  88. package/src/document/processActions/discard.ts +63 -0
  89. package/src/document/processActions/edit.ts +154 -0
  90. package/src/document/processActions/processActions.ts +218 -0
  91. package/src/document/processActions/publish.ts +120 -0
  92. package/src/document/processActions/releaseArchive.ts +77 -0
  93. package/src/document/processActions/releaseCreate.ts +59 -0
  94. package/src/document/processActions/releaseDelete.ts +65 -0
  95. package/src/document/processActions/releaseEdit.ts +37 -0
  96. package/src/document/processActions/releasePublish.ts +45 -0
  97. package/src/document/processActions/releaseSchedule.ts +87 -0
  98. package/src/document/processActions/releaseUtil.ts +31 -0
  99. package/src/document/processActions/shared.ts +308 -0
  100. package/src/document/processActions/unpublish.ts +85 -0
  101. package/src/document/processActions.test.ts +1129 -121
  102. package/src/document/reducers.test.ts +639 -64
  103. package/src/document/reducers.ts +187 -48
  104. package/src/document/resourceRules.test.ts +178 -0
  105. package/src/document/resourceRules.ts +117 -0
  106. package/src/document/sharedListener.test.ts +16 -14
  107. package/src/document/sharedListener.ts +24 -8
  108. package/src/document/util.ts +14 -0
  109. package/src/favorites/favorites.test.ts +5 -6
  110. package/src/installations/installations.test-d.ts +68 -0
  111. package/src/installations/installations.test.ts +62 -0
  112. package/src/installations/installations.ts +202 -0
  113. package/src/organization/organization.test-d.ts +36 -0
  114. package/src/organization/organization.test.ts +138 -0
  115. package/src/organization/organization.ts +138 -0
  116. package/src/organizations/organizations.test-d.ts +56 -0
  117. package/src/organizations/organizations.test.ts +150 -0
  118. package/src/organizations/organizations.ts +107 -0
  119. package/src/presence/bifurTransport.test.ts +46 -6
  120. package/src/presence/bifurTransport.ts +19 -2
  121. package/src/presence/presenceStore.test.ts +94 -23
  122. package/src/presence/presenceStore.ts +85 -29
  123. package/src/preview/getPreviewState.test.ts +137 -0
  124. package/src/preview/getPreviewState.ts +69 -0
  125. package/src/preview/previewProjectionUtils.test.ts +9 -22
  126. package/src/preview/previewProjectionUtils.ts +7 -3
  127. package/src/preview/resolvePreview.test.ts +64 -0
  128. package/src/preview/resolvePreview.ts +43 -0
  129. package/src/preview/types.ts +10 -0
  130. package/src/project/project.test-d.ts +37 -0
  131. package/src/project/project.test.ts +157 -12
  132. package/src/project/project.ts +138 -25
  133. package/src/projection/getProjectionState.test.ts +18 -20
  134. package/src/projection/getProjectionState.ts +23 -3
  135. package/src/projection/projectionQuery.ts +5 -5
  136. package/src/projection/projectionStore.test.ts +1 -1
  137. package/src/projection/resolveProjection.test.ts +1 -2
  138. package/src/projection/resolveProjection.ts +16 -0
  139. package/src/projection/subscribeToStateAndFetchBatches.test.ts +1 -1
  140. package/src/projection/subscribeToStateAndFetchBatches.ts +32 -23
  141. package/src/projection/types.ts +8 -0
  142. package/src/projects/projects.test-d.ts +32 -0
  143. package/src/projects/projects.test.ts +108 -42
  144. package/src/projects/projects.ts +63 -27
  145. package/src/query/queryStore.test.ts +259 -123
  146. package/src/query/queryStore.ts +78 -63
  147. package/src/query/reducers.test.ts +0 -42
  148. package/src/query/reducers.ts +2 -18
  149. package/src/releases/getPerspectiveState.test.ts +17 -23
  150. package/src/releases/getPerspectiveState.ts +8 -20
  151. package/src/releases/observeReleases.test.ts +127 -0
  152. package/src/releases/observeReleases.ts +102 -0
  153. package/src/releases/releasesStore.test.ts +100 -53
  154. package/src/releases/releasesStore.ts +69 -44
  155. package/src/releases/utils/sortReleases.test.ts +38 -2
  156. package/src/releases/utils/sortReleases.ts +15 -10
  157. package/src/store/createActionBinder.test.ts +87 -90
  158. package/src/store/createActionBinder.ts +79 -25
  159. package/src/store/createSanityInstance.test.ts +83 -13
  160. package/src/store/createSanityInstance.ts +77 -2
  161. package/src/store/createStateSourceAction.test.ts +64 -2
  162. package/src/store/createStateSourceAction.ts +39 -44
  163. package/src/store/createStoreInstance.test.ts +4 -2
  164. package/src/store/fetcherStore.test-d.ts +121 -0
  165. package/src/store/fetcherStore.test.ts +299 -0
  166. package/src/store/fetcherStore.ts +801 -0
  167. package/src/telemetry/environment.test.ts +119 -0
  168. package/src/telemetry/environment.ts +92 -0
  169. package/src/telemetry/events.ts +42 -0
  170. package/src/telemetry/initTelemetry.test.ts +432 -0
  171. package/src/telemetry/initTelemetry.ts +218 -0
  172. package/src/telemetry/telemetryManager.test.ts +327 -0
  173. package/src/telemetry/telemetryManager.ts +199 -0
  174. package/src/users/reducers.test.ts +6 -1
  175. package/src/users/reducers.ts +5 -6
  176. package/src/users/types.ts +4 -4
  177. package/src/users/usersStore.test.ts +16 -12
  178. package/src/users/usersStore.ts +5 -1
  179. package/src/utils/buildQuery.test.ts +14 -0
  180. package/src/utils/buildQuery.ts +17 -0
  181. package/src/utils/createFetcherStore.test.ts +7 -5
  182. package/src/utils/createFetcherStore.ts +8 -5
  183. package/src/utils/getStagingApiHost.test.ts +21 -0
  184. package/src/utils/getStagingApiHost.ts +14 -0
  185. package/src/utils/ids.test.ts +1 -29
  186. package/src/utils/ids.ts +0 -10
  187. package/src/utils/includeShape.ts +36 -0
  188. package/src/utils/isImportError.test.ts +72 -0
  189. package/src/utils/isImportError.ts +34 -0
  190. package/src/utils/logger.test.ts +12 -0
  191. package/src/utils/logger.ts +8 -3
  192. package/src/utils/object.test.ts +95 -0
  193. package/src/utils/object.ts +142 -0
  194. package/src/utils/setCleanupTimeout.ts +37 -0
  195. package/src/document/processActions.ts +0 -707
  196. package/src/utils/defineIntent.test.ts +0 -477
  197. package/src/utils/defineIntent.ts +0 -244
@@ -0,0 +1,72 @@
1
+ import {describe, expect, test} from 'vitest'
2
+
3
+ import {isImportError} from './isImportError'
4
+
5
+ describe('isImportError', () => {
6
+ test('returns false for non-Error values', () => {
7
+ expect(isImportError(null)).toBe(false)
8
+ expect(isImportError(undefined)).toBe(false)
9
+ expect(isImportError('Loading chunk 5 failed.')).toBe(false)
10
+ expect(isImportError({message: 'Loading chunk 5 failed.'})).toBe(false)
11
+ expect(isImportError(42)).toBe(false)
12
+ })
13
+
14
+ test('returns false for unrelated Error instances', () => {
15
+ expect(isImportError(new Error('Something else went wrong'))).toBe(false)
16
+ expect(isImportError(new TypeError('Cannot read properties of undefined'))).toBe(false)
17
+ })
18
+
19
+ test('detects webpack ChunkLoadError by name', () => {
20
+ const err = new Error('arbitrary message')
21
+ err.name = 'ChunkLoadError'
22
+ expect(isImportError(err)).toBe(true)
23
+ })
24
+
25
+ test('detects webpack numeric chunk failure messages', () => {
26
+ expect(isImportError(new Error('Loading chunk 5 failed.'))).toBe(true)
27
+ expect(
28
+ isImportError(new Error('Loading chunk 42 failed. (missing: https://x.com/42.abc.js)')),
29
+ ).toBe(true)
30
+ })
31
+
32
+ test('detects webpack named chunk failure messages', () => {
33
+ expect(isImportError(new Error('Loading chunk vendors-foo failed.'))).toBe(true)
34
+ expect(isImportError(new Error('Loading chunk react_vendors failed.'))).toBe(true)
35
+ })
36
+
37
+ test('detects Vite "Failed to fetch dynamically imported module"', () => {
38
+ expect(
39
+ isImportError(
40
+ new TypeError(
41
+ 'Failed to fetch dynamically imported module: https://example.com/assets/Home-abc123.js',
42
+ ),
43
+ ),
44
+ ).toBe(true)
45
+ })
46
+
47
+ test('detects Firefox "error loading dynamically imported module"', () => {
48
+ expect(
49
+ isImportError(
50
+ new TypeError(
51
+ 'error loading dynamically imported module: http://localhost:8080/src/views/Dashboard/index.vue',
52
+ ),
53
+ ),
54
+ ).toBe(true)
55
+ })
56
+
57
+ test('detects Safari module-script failures with and without the "ing" suffix', () => {
58
+ expect(isImportError(new TypeError('Importing a module script failed.'))).toBe(true)
59
+ expect(isImportError(new TypeError('Import a module script failed.'))).toBe(true)
60
+ })
61
+
62
+ test('detects Vite CSS preload failures', () => {
63
+ expect(isImportError(new Error('Unable to preload CSS for /assets/App-BBLnt7oG.css'))).toBe(
64
+ true,
65
+ )
66
+ })
67
+
68
+ test('matches case-insensitively', () => {
69
+ expect(isImportError(new Error('loading chunk 1 FAILED'))).toBe(true)
70
+ expect(isImportError(new Error('FAILED TO FETCH DYNAMICALLY IMPORTED MODULE'))).toBe(true)
71
+ })
72
+ })
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Returns true when the given error looks like a dynamic-import or
3
+ * code-split chunk-loading failure.
4
+ *
5
+ * These errors typically surface when a user has a tab open against a
6
+ * previously-deployed version of an app and the JavaScript or CSS chunk
7
+ * filenames have since changed: a fresh deployment removes the hashed assets
8
+ * the open tab still references. Detecting them lets the SDK trigger an
9
+ * automatic reload so the user gets the new build without manual intervention.
10
+ *
11
+ * Recognized shapes (webpack ChunkLoadError, Vite "Failed to fetch
12
+ * dynamically imported module", Firefox "error loading dynamically imported
13
+ * module", Safari "Importing a module script failed", and Vite "Unable to
14
+ * preload CSS").
15
+ *
16
+ * @param error - The value to inspect. Anything that is not an Error
17
+ * instance returns false.
18
+ * @returns True if the error matches a known import/chunk-load failure.
19
+ *
20
+ * @public
21
+ */
22
+ export function isImportError(error: unknown): boolean {
23
+ if (!(error instanceof Error)) return false
24
+ if (error.name === 'ChunkLoadError') return true
25
+
26
+ const message = error.message || ''
27
+ return (
28
+ /Loading chunk [\w-]+ failed/i.test(message) ||
29
+ /Failed to fetch dynamically imported module/i.test(message) ||
30
+ /error loading dynamically imported module/i.test(message) ||
31
+ /Import(?:ing)? a module script failed/i.test(message) ||
32
+ /Unable to preload CSS/i.test(message)
33
+ )
34
+ }
@@ -644,6 +644,7 @@ describe('logger', () => {
644
644
  instanceContext: {
645
645
  instanceId: 'abc123def456',
646
646
  projectId: 'my-project',
647
+ dataset: 'production',
647
648
  },
648
649
  })
649
650
 
@@ -653,6 +654,10 @@ describe('logger', () => {
653
654
  expect.stringContaining('[project:my-project]'),
654
655
  expect.any(Object),
655
656
  )
657
+ expect(mockHandler.info).toHaveBeenCalledWith(
658
+ expect.stringContaining('[dataset:production]'),
659
+ expect.any(Object),
660
+ )
656
661
  expect(mockHandler.info).toHaveBeenCalledWith(
657
662
  expect.stringContaining('[instance:abc123de]'), // Truncated to 8 chars
658
663
  expect.any(Object),
@@ -689,6 +694,7 @@ describe('logger', () => {
689
694
  const logger = createLogger('query', {
690
695
  instanceContext: {
691
696
  projectId: 'my-project',
697
+ // No dataset or instanceId
692
698
  },
693
699
  })
694
700
 
@@ -698,12 +704,17 @@ describe('logger', () => {
698
704
  expect.stringContaining('[project:my-project]'),
699
705
  expect.any(Object),
700
706
  )
707
+ expect(mockHandler.info).not.toHaveBeenCalledWith(
708
+ expect.stringContaining('[dataset:'),
709
+ expect.any(Object),
710
+ )
701
711
  })
702
712
 
703
713
  it('should return instance context', () => {
704
714
  const instanceContext = {
705
715
  instanceId: 'test-instance',
706
716
  projectId: 'test-project',
717
+ dataset: 'test-dataset',
707
718
  }
708
719
 
709
720
  const logger = createLogger('auth', {instanceContext})
@@ -727,6 +738,7 @@ describe('logger', () => {
727
738
  const logger = createLogger('document', {
728
739
  instanceContext: {
729
740
  projectId: 'my-project',
741
+ dataset: 'production',
730
742
  },
731
743
  })
732
744
 
@@ -153,9 +153,9 @@ export interface LogContext {
153
153
  export interface InstanceContext {
154
154
  /** Unique instance ID */
155
155
  instanceId?: string
156
- /** Project ID (derived from default resource) */
156
+ /** Project ID */
157
157
  projectId?: string
158
- /** Dataset name (derived from default resource) */
158
+ /** Dataset name */
159
159
  dataset?: string
160
160
  }
161
161
 
@@ -520,9 +520,14 @@ function logRxJSOperator(namespace: string, operator: string, context?: LogConte
520
520
  * @internal
521
521
  */
522
522
  /* c8 ignore next 7 */
523
- function getInstanceContext(instance: {instanceId?: string}): InstanceContext {
523
+ function getInstanceContext(instance: {
524
+ instanceId?: string
525
+ config?: {projectId?: string; dataset?: string}
526
+ }): InstanceContext {
524
527
  return {
525
528
  instanceId: instance.instanceId,
529
+ projectId: instance.config?.projectId,
530
+ dataset: instance.config?.dataset,
526
531
  }
527
532
  }
528
533
 
@@ -0,0 +1,95 @@
1
+ import {describe, expect, it} from 'vitest'
2
+
3
+ import {isDeepEqual, isObject, omitProperty, pickProperties} from './object'
4
+
5
+ describe('object utils', () => {
6
+ describe('isObject', () => {
7
+ it('returns true for objects and false for primitives', () => {
8
+ expect(isObject({foo: 'bar'})).toBe(true)
9
+ expect(isObject(null)).toBe(false)
10
+ expect(isObject('hello')).toBe(false)
11
+ })
12
+ })
13
+
14
+ describe('omitProperty', () => {
15
+ it('removes a property from an object copy', () => {
16
+ expect(omitProperty({foo: 'bar', baz: 1}, 'foo')).toEqual({baz: 1})
17
+ })
18
+
19
+ it('returns an empty object for undefined input', () => {
20
+ expect(omitProperty<{foo: string}, 'foo'>(undefined, 'foo')).toEqual({})
21
+ })
22
+ })
23
+
24
+ describe('pickProperties', () => {
25
+ it('copies only the requested own properties', () => {
26
+ expect(pickProperties({foo: 'bar', baz: 1}, ['foo'])).toEqual({foo: 'bar'})
27
+ })
28
+ })
29
+
30
+ describe('isDeepEqual', () => {
31
+ it('matches nested plain objects and arrays', () => {
32
+ expect(
33
+ isDeepEqual({foo: [{bar: 'baz'}], qux: {count: 2}}, {foo: [{bar: 'baz'}], qux: {count: 2}}),
34
+ ).toBe(true)
35
+ })
36
+
37
+ it('returns false for unequal nested plain objects and arrays', () => {
38
+ expect(
39
+ isDeepEqual(
40
+ {foo: [{bar: 'baz'}], qux: {count: 2}},
41
+ {foo: [{bar: 'nope'}], qux: {count: 2}},
42
+ ),
43
+ ).toBe(false)
44
+ expect(isDeepEqual([1, {foo: 'bar'}], [1, {foo: 'baz'}])).toBe(false)
45
+ })
46
+
47
+ it('matches sets and maps with equal contents', () => {
48
+ expect(isDeepEqual(new Set([{foo: 'bar'}, 'baz']), new Set(['baz', {foo: 'bar'}]))).toBe(true)
49
+
50
+ expect(
51
+ isDeepEqual(
52
+ new Map<string, unknown>([
53
+ ['foo', {bar: 'baz'}],
54
+ ['count', 2],
55
+ ]),
56
+ new Map<string, unknown>([
57
+ ['foo', {bar: 'baz'}],
58
+ ['count', 2],
59
+ ]),
60
+ ),
61
+ ).toBe(true)
62
+ })
63
+
64
+ it('compares dates by timestamp', () => {
65
+ expect(isDeepEqual(new Date('2024-01-01'), new Date('2024-01-01'))).toBe(true)
66
+ expect(isDeepEqual(new Date('2024-01-01'), new Date('2024-01-02'))).toBe(false)
67
+ })
68
+
69
+ it('compares regular expressions by source and flags', () => {
70
+ expect(isDeepEqual(/foo/gi, /foo/gi)).toBe(true)
71
+ expect(isDeepEqual(/foo/g, /foo/i)).toBe(false)
72
+ expect(isDeepEqual(/foo/g, /bar/g)).toBe(false)
73
+ })
74
+
75
+ it('returns false for cross-shape mismatches', () => {
76
+ expect(isDeepEqual({foo: 'bar'}, ['foo', 'bar'] as unknown as {foo: string})).toBe(false)
77
+ expect(
78
+ isDeepEqual(
79
+ new Map<string, unknown>([['foo', 'bar']]) as unknown as object,
80
+ new Set(['foo', 'bar']) as unknown as object,
81
+ ),
82
+ ).toBe(false)
83
+ })
84
+
85
+ it('treats non-plain objects as unequal unless they are the same reference', () => {
86
+ class Example {
87
+ constructor(public value: string) {}
88
+ }
89
+
90
+ expect(isDeepEqual(new Example('a'), new Example('a'))).toBe(false)
91
+ const instance = new Example('a')
92
+ expect(isDeepEqual(instance, instance)).toBe(true)
93
+ })
94
+ })
95
+ })
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Returns true when the input is a non-null object.
3
+ *
4
+ * @internal
5
+ */
6
+ export function isObject(value: unknown): value is object {
7
+ return typeof value === 'object' && value !== null
8
+ }
9
+
10
+ const hasOwn = (value: object, key: PropertyKey) => Object.prototype.hasOwnProperty.call(value, key)
11
+
12
+ const isPlainObject = (value: unknown): value is Record<string, unknown> => {
13
+ if (!isObject(value)) return false
14
+
15
+ const prototype = Object.getPrototypeOf(value)
16
+ return prototype === Object.prototype || prototype === null
17
+ }
18
+
19
+ /**
20
+ * Creates a shallow copy without the provided property.
21
+ *
22
+ * @internal
23
+ */
24
+ export function omitProperty<T extends object, K extends keyof T>(
25
+ value: T | null | undefined,
26
+ key: K,
27
+ ): Omit<T, K> {
28
+ if (!value) return {} as Omit<T, K>
29
+
30
+ const {[key]: _omitted, ...rest} = value
31
+ return rest
32
+ }
33
+
34
+ /**
35
+ * Creates a shallow copy containing only the provided properties.
36
+ *
37
+ * @internal
38
+ */
39
+ export function pickProperties<T extends object, K extends keyof T>(
40
+ value: T,
41
+ keys: readonly K[],
42
+ ): Pick<T, K> {
43
+ const result = {} as Pick<T, K>
44
+
45
+ for (const key of keys) {
46
+ if (hasOwn(value, key)) {
47
+ result[key] = value[key]
48
+ }
49
+ }
50
+
51
+ return result
52
+ }
53
+
54
+ const areSetsEqual = (left: Set<unknown>, right: Set<unknown>): boolean => {
55
+ if (left.size !== right.size) return false
56
+
57
+ const unmatched = [...right]
58
+
59
+ outer: for (const leftValue of left) {
60
+ for (let index = 0; index < unmatched.length; index++) {
61
+ if (isDeepEqual(leftValue, unmatched[index])) {
62
+ unmatched.splice(index, 1)
63
+ continue outer
64
+ }
65
+ }
66
+
67
+ return false
68
+ }
69
+
70
+ return unmatched.length === 0
71
+ }
72
+
73
+ const areMapsEqual = (left: Map<unknown, unknown>, right: Map<unknown, unknown>): boolean => {
74
+ if (left.size !== right.size) return false
75
+
76
+ const unmatched = [...right.entries()]
77
+
78
+ outer: for (const [leftKey, leftValue] of left) {
79
+ for (let index = 0; index < unmatched.length; index++) {
80
+ const [rightKey, rightValue] = unmatched[index]
81
+
82
+ if (isDeepEqual(leftKey, rightKey) && isDeepEqual(leftValue, rightValue)) {
83
+ unmatched.splice(index, 1)
84
+ continue outer
85
+ }
86
+ }
87
+
88
+ return false
89
+ }
90
+
91
+ return unmatched.length === 0
92
+ }
93
+
94
+ /**
95
+ * Compares values deeply across the plain object, array, map, and set shapes used by the SDK.
96
+ * This helper is intended for acyclic SDK data structures and does not guard against circular
97
+ * references.
98
+ *
99
+ * @internal
100
+ */
101
+ export function isDeepEqual<T>(left: T, right: T): boolean {
102
+ if (Object.is(left, right)) return true
103
+
104
+ if (!isObject(left) || !isObject(right)) return false
105
+
106
+ if (left instanceof Date && right instanceof Date) {
107
+ return left.getTime() === right.getTime()
108
+ }
109
+
110
+ if (left instanceof RegExp && right instanceof RegExp) {
111
+ return left.source === right.source && left.flags === right.flags
112
+ }
113
+
114
+ if (left instanceof Set && right instanceof Set) {
115
+ return areSetsEqual(left, right)
116
+ }
117
+
118
+ if (left instanceof Map && right instanceof Map) {
119
+ return areMapsEqual(left, right)
120
+ }
121
+
122
+ if (Array.isArray(left) || Array.isArray(right)) {
123
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false
124
+
125
+ return left.every((value, index) => isDeepEqual(value, right[index]))
126
+ }
127
+
128
+ if (!isPlainObject(left) || !isPlainObject(right)) return false
129
+
130
+ const leftKeys = Object.keys(left)
131
+ const rightKeys = Object.keys(right)
132
+
133
+ if (leftKeys.length !== rightKeys.length) return false
134
+
135
+ for (const key of leftKeys) {
136
+ if (!hasOwn(right, key) || !isDeepEqual(left[key], right[key])) {
137
+ return false
138
+ }
139
+ }
140
+
141
+ return true
142
+ }
@@ -0,0 +1,37 @@
1
+ // Calls `.unref()` on a timer when running in Node.js. In Node.js, timers are
2
+ // objects with an `unref()` method; in browsers they are numbers. Narrow at
3
+ // runtime so this works in both environments without type assertions.
4
+ function unref(timer: ReturnType<typeof setTimeout> | ReturnType<typeof setInterval>): void {
5
+ const t: unknown = timer
6
+ if (typeof t === 'object' && t !== null && 'unref' in t && typeof t.unref === 'function') {
7
+ t.unref()
8
+ }
9
+ }
10
+
11
+ /**
12
+ * Like `setTimeout`, but calls `.unref()` on the timer when running in Node.js.
13
+ *
14
+ * In Node.js, active timers prevent the process from exiting. Cleanup timers
15
+ * (e.g. deferred subscription removal) should not keep the process alive -
16
+ * `.unref()` lets the process exit naturally while still firing the timer if
17
+ * the process happens to still be running.
18
+ *
19
+ * In browsers, `setTimeout` returns a number and has no `.unref()` method,
20
+ * so this is a no-op there.
21
+ */
22
+ export function setCleanupTimeout(fn: () => void, delay: number): ReturnType<typeof setTimeout> {
23
+ const timer = setTimeout(fn, delay)
24
+ unref(timer)
25
+ return timer
26
+ }
27
+
28
+ /**
29
+ * Like `setInterval`, but calls `.unref()` on the timer when running in Node.js
30
+ * so a recurring poll never keeps the process alive. See {@link setCleanupTimeout}
31
+ * for why cleanup and background timers must not block process exit.
32
+ */
33
+ export function setCleanupInterval(fn: () => void, delay: number): ReturnType<typeof setInterval> {
34
+ const timer = setInterval(fn, delay)
35
+ unref(timer)
36
+ return timer
37
+ }