@sanity/sdk 3.0.0-rc.0 → 3.0.0-rc.2
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/README.md +12 -0
- package/dist/_exports/_internal.d.ts +163 -0
- package/dist/_exports/_internal.d.ts.map +1 -0
- package/dist/_exports/_internal.js +332 -0
- package/dist/_exports/_internal.js.map +1 -0
- package/dist/_exports/agent.d.ts +65 -0
- package/dist/_exports/agent.d.ts.map +1 -0
- package/dist/_exports/agent.js +71 -0
- package/dist/_exports/agent.js.map +1 -0
- package/dist/_exports/comlink.d.ts +118 -0
- package/dist/_exports/comlink.d.ts.map +1 -0
- package/dist/_exports/comlink.js +69 -0
- package/dist/_exports/comlink.js.map +1 -0
- package/dist/authStore-CtE9bLHW.js +754 -0
- package/dist/authStore-CtE9bLHW.js.map +1 -0
- package/dist/authStore-DKFX8fn7.d.ts +3179 -0
- package/dist/authStore-DKFX8fn7.d.ts.map +1 -0
- package/dist/clientStore-BtE2-q2i.js +90 -0
- package/dist/clientStore-BtE2-q2i.js.map +1 -0
- package/dist/createSanityInstance-DUI1bGIg.d.ts +278 -0
- package/dist/createSanityInstance-DUI1bGIg.d.ts.map +1 -0
- package/dist/createStateSourceAction-BOKtqFNm.js +300 -0
- package/dist/createStateSourceAction-BOKtqFNm.js.map +1 -0
- package/dist/getEnv-D5RGkQLu.js +8 -0
- package/dist/getEnv-D5RGkQLu.js.map +1 -0
- package/dist/getNodeState-CR3__B2D.js +67 -0
- package/dist/getNodeState-CR3__B2D.js.map +1 -0
- package/dist/index.d.ts +3 -2332
- package/dist/index.js +4171 -4332
- package/dist/index.js.map +1 -1
- package/dist/logger-DAtJb5RQ.js +173 -0
- package/dist/logger-DAtJb5RQ.js.map +1 -0
- package/dist/object-CxXlSVg7.js +83 -0
- package/dist/object-CxXlSVg7.js.map +1 -0
- package/dist/queryStore-DPf9sN3V.js +1346 -0
- package/dist/queryStore-DPf9sN3V.js.map +1 -0
- package/dist/rolldown-runtime-vyAXikos.js +9 -0
- package/dist/setCleanupTimeout-BiM6q1KF.js +31 -0
- package/dist/setCleanupTimeout-BiM6q1KF.js.map +1 -0
- package/dist/telemetryManager-Dq_qCkeI.js +127 -0
- package/dist/telemetryManager-Dq_qCkeI.js.map +1 -0
- package/dist/version-BChjsi4u.js +9 -0
- package/dist/version-BChjsi4u.js.map +1 -0
- package/package.json +47 -52
- package/src/_exports/_internal.ts +27 -0
- package/src/_exports/agent.ts +19 -0
- package/src/_exports/comlink.ts +16 -0
- package/src/_exports/index.ts +125 -64
- package/src/access/checkPermissions.test.ts +55 -0
- package/src/access/checkPermissions.ts +54 -0
- package/src/agent/agentActions.test.ts +16 -60
- package/src/agent/agentActions.ts +25 -38
- package/src/applications/applications.test-d.ts +88 -0
- package/src/applications/applications.test.ts +130 -0
- package/src/applications/applications.ts +367 -0
- package/src/auth/authLogger.ts +30 -0
- package/src/auth/authStore.test.ts +194 -13
- package/src/auth/authStore.ts +58 -26
- package/src/auth/getOrganizationVerificationState.test.ts +18 -17
- package/src/auth/getOrganizationVerificationState.ts +4 -4
- package/src/auth/handleAuthCallback.test.ts +35 -1
- package/src/auth/handleAuthCallback.ts +25 -6
- package/src/auth/logout.test.ts +75 -2
- package/src/auth/logout.ts +23 -4
- package/src/auth/refreshStampedToken.test.ts +219 -469
- package/src/auth/refreshStampedToken.ts +79 -195
- package/src/auth/studioAuth.ts +1 -1
- package/src/auth/studioModeAuth.ts +2 -1
- package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +146 -13
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +37 -6
- package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +2 -0
- package/src/client/clientStore.test.ts +62 -37
- package/src/client/clientStore.ts +45 -75
- package/src/client/liveEvents.test.ts +151 -0
- package/src/client/liveEvents.ts +107 -0
- package/src/comlink/controller/actions/destroyController.test.ts +4 -1
- package/src/comlink/controller/actions/getOrCreateChannel.test.ts +4 -1
- package/src/comlink/controller/actions/getOrCreateChannel.ts +2 -2
- package/src/comlink/controller/actions/getOrCreateController.test.ts +4 -1
- package/src/comlink/controller/actions/releaseChannel.test.ts +1 -1
- package/src/comlink/controller/comlinkControllerStore.test.ts +4 -1
- package/src/comlink/node/actions/getOrCreateNode.test.ts +9 -3
- package/src/comlink/node/actions/getOrCreateNode.ts +2 -2
- package/src/comlink/node/actions/releaseNode.test.ts +7 -4
- package/src/comlink/node/comlinkNodeStore.test.ts +2 -2
- package/src/comlink/node/getNodeState.test.ts +1 -1
- package/src/comlink/node/getNodeState.ts +2 -1
- package/src/comments/addonDatasetStore.test.ts +215 -0
- package/src/comments/addonDatasetStore.ts +249 -0
- package/src/comments/buildCommentThreads.test.ts +83 -0
- package/src/comments/buildCommentThreads.ts +54 -0
- package/src/comments/commentActions.test.ts +540 -0
- package/src/comments/commentActions.ts +555 -0
- package/src/comments/commentFieldPath.test.ts +40 -0
- package/src/comments/commentFieldPath.ts +44 -0
- package/src/comments/commentsConstants.ts +80 -0
- package/src/comments/commentsStore.test.ts +419 -0
- package/src/comments/commentsStore.ts +483 -0
- package/src/comments/normalizeComment.test.ts +116 -0
- package/src/comments/normalizeComment.ts +45 -0
- package/src/comments/observeComments.test.ts +193 -0
- package/src/comments/observeComments.ts +113 -0
- package/src/comments/reducers.test.ts +277 -0
- package/src/comments/reducers.ts +316 -0
- package/src/comments/types.ts +249 -0
- package/src/comments/weakenReferences.test.ts +47 -0
- package/src/comments/weakenReferences.ts +29 -0
- package/src/config/__tests__/handles.test.ts +18 -12
- package/src/config/handles.ts +25 -7
- package/src/config/sanityConfig.ts +40 -82
- package/src/datasets/datasets.test.ts +2 -2
- package/src/datasets/datasets.ts +10 -4
- package/src/document/actions.test.ts +115 -33
- package/src/document/actions.ts +212 -15
- package/src/document/applyDocumentActions.test.ts +35 -13
- package/src/document/applyDocumentActions.ts +30 -9
- package/src/document/documentConstants.ts +15 -0
- package/src/document/documentStore.concurrency.test.ts +1306 -0
- package/src/document/documentStore.test.ts +753 -273
- package/src/document/documentStore.ts +197 -75
- package/src/document/events.test.ts +57 -2
- package/src/document/events.ts +82 -26
- package/src/document/listen.test.ts +56 -0
- package/src/document/listen.ts +74 -21
- package/src/document/listenerEventOperators.test.ts +311 -0
- package/src/document/listenerEventOperators.ts +217 -0
- package/src/document/patchOperations.test.ts +76 -2
- package/src/document/patchOperations.ts +60 -1
- package/src/document/permissions.test.ts +107 -60
- package/src/document/permissions.ts +44 -19
- package/src/document/processActions/create.ts +138 -0
- package/src/document/processActions/delete.ts +100 -0
- package/src/document/processActions/discard.ts +63 -0
- package/src/document/processActions/edit.ts +154 -0
- package/src/document/processActions/processActions.ts +218 -0
- package/src/document/processActions/publish.ts +120 -0
- package/src/document/processActions/releaseArchive.ts +77 -0
- package/src/document/processActions/releaseCreate.ts +59 -0
- package/src/document/processActions/releaseDelete.ts +65 -0
- package/src/document/processActions/releaseEdit.ts +37 -0
- package/src/document/processActions/releasePublish.ts +45 -0
- package/src/document/processActions/releaseSchedule.ts +87 -0
- package/src/document/processActions/releaseUtil.ts +31 -0
- package/src/document/processActions/shared.ts +308 -0
- package/src/document/processActions/unpublish.ts +85 -0
- package/src/document/processActions.test.ts +1129 -121
- package/src/document/reducers.test.ts +772 -64
- package/src/document/reducers.ts +205 -51
- package/src/document/resourceRules.test.ts +178 -0
- package/src/document/resourceRules.ts +117 -0
- package/src/document/sharedListener.test.ts +16 -14
- package/src/document/sharedListener.ts +24 -8
- package/src/document/util.test.ts +46 -0
- package/src/document/util.ts +50 -0
- package/src/favorites/favorites.test.ts +5 -6
- package/src/installations/installations.test-d.ts +73 -0
- package/src/installations/installations.test.ts +62 -0
- package/src/installations/installations.ts +206 -0
- package/src/organization/organization.test-d.ts +36 -0
- package/src/organization/organization.test.ts +138 -0
- package/src/organization/organization.ts +138 -0
- package/src/organizations/organizations.test-d.ts +56 -0
- package/src/organizations/organizations.test.ts +150 -0
- package/src/organizations/organizations.ts +107 -0
- package/src/presence/bifurTransport.test.ts +201 -9
- package/src/presence/bifurTransport.ts +98 -14
- package/src/presence/paths.test.ts +70 -0
- package/src/presence/paths.ts +41 -0
- package/src/presence/presenceStore.test.ts +915 -24
- package/src/presence/presenceStore.ts +576 -104
- package/src/presence/types.ts +175 -3
- package/src/preview/previewProjectionUtils.test.ts +9 -22
- package/src/preview/previewProjectionUtils.ts +7 -3
- package/src/preview/types.ts +0 -15
- package/src/project/project.test-d.ts +37 -0
- package/src/project/project.test.ts +157 -12
- package/src/project/project.ts +138 -25
- package/src/projection/getProjectionState.test.ts +18 -20
- package/src/projection/getProjectionState.ts +23 -3
- package/src/projection/projectionQuery.ts +5 -5
- package/src/projection/projectionStore.test.ts +1 -9
- package/src/projection/resolveProjection.test.ts +1 -2
- package/src/projection/resolveProjection.ts +16 -0
- package/src/projection/subscribeToStateAndFetchBatches.test.ts +1 -1
- package/src/projection/subscribeToStateAndFetchBatches.ts +32 -23
- package/src/projects/projects.test-d.ts +32 -0
- package/src/projects/projects.test.ts +108 -42
- package/src/projects/projects.ts +63 -27
- package/src/query/queryStore.test.ts +259 -123
- package/src/query/queryStore.ts +85 -69
- package/src/query/reducers.test.ts +0 -42
- package/src/query/reducers.ts +2 -18
- package/src/releases/getPerspectiveState.test.ts +17 -23
- package/src/releases/getPerspectiveState.ts +8 -20
- package/src/releases/observeReleases.test.ts +127 -0
- package/src/releases/observeReleases.ts +102 -0
- package/src/releases/releasesStore.test.ts +100 -53
- package/src/releases/releasesStore.ts +69 -44
- package/src/releases/utils/sortReleases.test.ts +38 -2
- package/src/releases/utils/sortReleases.ts +15 -10
- package/src/store/createActionBinder.test.ts +109 -90
- package/src/store/createActionBinder.ts +36 -28
- package/src/store/createSanityInstance.test.ts +15 -13
- package/src/store/createSanityInstance.ts +14 -2
- package/src/store/createStateSourceAction.test.ts +64 -2
- package/src/store/createStateSourceAction.ts +38 -43
- package/src/store/createStoreInstance.test.ts +4 -2
- package/src/store/fetcherStore.test-d.ts +121 -0
- package/src/store/fetcherStore.test.ts +299 -0
- package/src/store/fetcherStore.ts +801 -0
- package/src/telemetry/environment.test.ts +119 -0
- package/src/telemetry/environment.ts +92 -0
- package/src/telemetry/events.ts +42 -0
- package/src/telemetry/initTelemetry.test.ts +432 -0
- package/src/telemetry/initTelemetry.ts +218 -0
- package/src/telemetry/telemetryManager.test.ts +327 -0
- package/src/telemetry/telemetryManager.ts +199 -0
- package/src/users/reducers.test.ts +3 -1
- package/src/users/reducers.ts +5 -6
- package/src/users/types.ts +4 -4
- package/src/users/usersStore.test.ts +16 -12
- package/src/users/usersStore.ts +5 -1
- package/src/utils/buildQuery.test.ts +14 -0
- package/src/utils/buildQuery.ts +17 -0
- package/src/utils/createFetcherStore.test.ts +7 -5
- package/src/utils/createFetcherStore.ts +8 -5
- package/src/utils/getStagingApiHost.test.ts +21 -0
- package/src/utils/getStagingApiHost.ts +14 -0
- package/src/utils/ids.test.ts +1 -29
- package/src/utils/ids.ts +0 -10
- package/src/utils/includeShape.ts +36 -0
- package/src/utils/isImportError.test.ts +72 -0
- package/src/utils/isImportError.ts +34 -0
- package/src/utils/logger.test.ts +12 -0
- package/src/utils/logger.ts +8 -3
- package/src/utils/object.test.ts +95 -0
- package/src/utils/object.ts +142 -0
- package/src/utils/setCleanupTimeout.ts +37 -0
- package/src/document/processActions.ts +0 -707
- package/src/utils/defineIntent.test.ts +0 -477
- package/src/utils/defineIntent.ts +0 -244
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import {type ListenEvent, type MutationEvent} from '@sanity/client'
|
|
2
|
+
import {type SanityDocument} from '@sanity/types'
|
|
3
|
+
import {defer, filter, type MonoTypeOperatorFunction, Observable} from 'rxjs'
|
|
4
|
+
|
|
5
|
+
import {setCleanupTimeout} from '../utils/setCleanupTimeout'
|
|
6
|
+
|
|
7
|
+
const DEDUPE_TTL = 120_000
|
|
8
|
+
const DEDUPE_MAX_ENTRIES = 1_000
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* How long to hold an incomplete multi-document transaction (and the events
|
|
12
|
+
* held behind it) before releasing everything anyway. Only reached when part
|
|
13
|
+
* of a transaction is lost.
|
|
14
|
+
*/
|
|
15
|
+
const GROUP_FLUSH_DEADLINE = 30_000
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* How many intervening events to hold behind an incomplete multi-document
|
|
19
|
+
* transaction before giving up on grouping and releasing everything in
|
|
20
|
+
* arrival order.
|
|
21
|
+
*/
|
|
22
|
+
const GROUP_MAX_HELD_EVENTS = 100
|
|
23
|
+
|
|
24
|
+
interface DedupeOptions {
|
|
25
|
+
ttl?: number
|
|
26
|
+
maxEntries?: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Drops mutation events that have already been observed, keyed by
|
|
31
|
+
* `transactionId#documentId`. With `enableResume`, the listener replays
|
|
32
|
+
* missed events after a reconnect on an at-least-once basis, so the same
|
|
33
|
+
* event can be delivered twice. A duplicate would never chain onto the
|
|
34
|
+
* current base revision and would clog the sequencing buffer until it
|
|
35
|
+
* overflows into an `OutOfSyncError`.
|
|
36
|
+
*
|
|
37
|
+
* Seen keys expire after `ttl` milliseconds and the cache is capped at
|
|
38
|
+
* `maxEntries`, evicting the oldest entries first.
|
|
39
|
+
*
|
|
40
|
+
* @internal
|
|
41
|
+
*/
|
|
42
|
+
export function dedupeListenerEvents(
|
|
43
|
+
options?: DedupeOptions,
|
|
44
|
+
): MonoTypeOperatorFunction<ListenEvent<SanityDocument>> {
|
|
45
|
+
const {ttl = DEDUPE_TTL, maxEntries = DEDUPE_MAX_ENTRIES} = options ?? {}
|
|
46
|
+
|
|
47
|
+
return (source) =>
|
|
48
|
+
defer(() => {
|
|
49
|
+
// insertion-ordered map of dedupe key -> expiry timestamp
|
|
50
|
+
const seen = new Map<string, number>()
|
|
51
|
+
|
|
52
|
+
return source.pipe(
|
|
53
|
+
filter((event) => {
|
|
54
|
+
if (event.type !== 'mutation') return true
|
|
55
|
+
|
|
56
|
+
const key = `${event.transactionId}#${event.documentId}`
|
|
57
|
+
const now = Date.now()
|
|
58
|
+
|
|
59
|
+
const expiry = seen.get(key)
|
|
60
|
+
if (expiry !== undefined && expiry > now) return false
|
|
61
|
+
|
|
62
|
+
seen.delete(key)
|
|
63
|
+
seen.set(key, now + ttl)
|
|
64
|
+
|
|
65
|
+
for (const [existingKey, existingExpiry] of seen) {
|
|
66
|
+
if (seen.size <= maxEntries && existingExpiry > now) break
|
|
67
|
+
seen.delete(existingKey)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return true
|
|
71
|
+
}),
|
|
72
|
+
)
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface GroupTransactionEventsOptions {
|
|
77
|
+
flushDeadline?: number
|
|
78
|
+
maxHeldEvents?: number
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Buffers the mutation events of multi-document transactions (e.g. a publish
|
|
83
|
+
* touching both the draft and the published document) until every event of
|
|
84
|
+
* the transaction has arrived, then emits them contiguously. Without this,
|
|
85
|
+
* a rebase can run between the two halves of a transaction and observe the
|
|
86
|
+
* dataset in a state that never existed on the server (draft deleted, but
|
|
87
|
+
* published not yet updated).
|
|
88
|
+
*
|
|
89
|
+
* The backend's streaming listener pipeline delivers a transaction's events
|
|
90
|
+
* contiguously and in order, but the legacy pipeline can interleave
|
|
91
|
+
* concurrent transactions. So while a multi-document transaction is
|
|
92
|
+
* incomplete, mutation events from other transactions are held (in arrival
|
|
93
|
+
* order) rather than emitted, and released right after the transaction
|
|
94
|
+
* completes; released events are re-processed so a held multi-document
|
|
95
|
+
* transaction is grouped too. Per-document `previousRev` sequencing
|
|
96
|
+
* downstream tolerates the reordering this introduces.
|
|
97
|
+
*
|
|
98
|
+
* Escape hatches so a lost event degrades to ungrouped delivery instead of
|
|
99
|
+
* stalling the stream: everything is released in arrival order when a
|
|
100
|
+
* non-mutation event (e.g. reconnect/welcome) arrives, when more than
|
|
101
|
+
* `maxHeldEvents` are held, or after `flushDeadline` milliseconds.
|
|
102
|
+
* Single-event transactions pass through untouched when nothing is buffered.
|
|
103
|
+
*
|
|
104
|
+
* @internal
|
|
105
|
+
*/
|
|
106
|
+
export function groupTransactionEvents(
|
|
107
|
+
options?: GroupTransactionEventsOptions,
|
|
108
|
+
): MonoTypeOperatorFunction<ListenEvent<SanityDocument>> {
|
|
109
|
+
const {flushDeadline = GROUP_FLUSH_DEADLINE, maxHeldEvents = GROUP_MAX_HELD_EVENTS} =
|
|
110
|
+
options ?? {}
|
|
111
|
+
|
|
112
|
+
return (source) =>
|
|
113
|
+
new Observable<ListenEvent<SanityDocument>>((subscriber) => {
|
|
114
|
+
interface OpenTransaction {
|
|
115
|
+
transactionId: string
|
|
116
|
+
events: MutationEvent[]
|
|
117
|
+
total: number
|
|
118
|
+
timer: ReturnType<typeof setTimeout>
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let open: OpenTransaction | null = null
|
|
122
|
+
let held: MutationEvent[] = []
|
|
123
|
+
|
|
124
|
+
// emits the open transaction's events (complete or not) without
|
|
125
|
+
// releasing the held queue; callers decide what happens to `held`
|
|
126
|
+
const closeOpen = () => {
|
|
127
|
+
if (!open) return
|
|
128
|
+
clearTimeout(open.timer)
|
|
129
|
+
const {events} = open
|
|
130
|
+
open = null
|
|
131
|
+
for (const event of events) subscriber.next(event)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// re-processes the held queue so a held multi-document transaction
|
|
135
|
+
// gets grouped in turn. processing may open a new transaction and top
|
|
136
|
+
// up a new held queue; each drain only touches its own snapshot, so
|
|
137
|
+
// this terminates
|
|
138
|
+
const releaseHeld = () => {
|
|
139
|
+
const queue = held
|
|
140
|
+
held = []
|
|
141
|
+
for (const event of queue) processEvent(event)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const releaseEverything = () => {
|
|
145
|
+
closeOpen()
|
|
146
|
+
const queue = held
|
|
147
|
+
held = []
|
|
148
|
+
for (const event of queue) subscriber.next(event)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const processEvent = (event: ListenEvent<SanityDocument>) => {
|
|
152
|
+
if (event.type !== 'mutation') {
|
|
153
|
+
// connection-level events (welcome/welcomeback/reset/reconnect)
|
|
154
|
+
// reset the world downstream; release everything before them
|
|
155
|
+
releaseEverything()
|
|
156
|
+
subscriber.next(event)
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const isMultiDocument = (event.transactionTotalEvents ?? 0) > 1
|
|
161
|
+
|
|
162
|
+
if (!open) {
|
|
163
|
+
if (!isMultiDocument) {
|
|
164
|
+
subscriber.next(event)
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
// isMultiDocument guarantees total >= 2, so a freshly opened
|
|
168
|
+
// transaction is always incomplete; completion is checked when
|
|
169
|
+
// subsequent events of the same transaction arrive below
|
|
170
|
+
open = {
|
|
171
|
+
transactionId: event.transactionId,
|
|
172
|
+
events: [event],
|
|
173
|
+
total: event.transactionTotalEvents,
|
|
174
|
+
timer: setCleanupTimeout(() => {
|
|
175
|
+
closeOpen()
|
|
176
|
+
releaseHeld()
|
|
177
|
+
}, flushDeadline),
|
|
178
|
+
}
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (event.transactionId === open.transactionId) {
|
|
183
|
+
open.events.push(event)
|
|
184
|
+
if (open.events.length >= open.total) {
|
|
185
|
+
closeOpen()
|
|
186
|
+
releaseHeld()
|
|
187
|
+
}
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// an event from another transaction while one is incomplete: hold it
|
|
192
|
+
// (legacy listener pipeline interleaving) instead of giving up on the
|
|
193
|
+
// open transaction
|
|
194
|
+
held.push(event)
|
|
195
|
+
if (held.length > maxHeldEvents) releaseEverything()
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const subscription = source.subscribe({
|
|
199
|
+
next: processEvent,
|
|
200
|
+
error: (error) => {
|
|
201
|
+
releaseEverything()
|
|
202
|
+
subscriber.error(error)
|
|
203
|
+
},
|
|
204
|
+
complete: () => {
|
|
205
|
+
releaseEverything()
|
|
206
|
+
subscriber.complete()
|
|
207
|
+
},
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
return () => {
|
|
211
|
+
if (open) clearTimeout(open.timer)
|
|
212
|
+
open = null
|
|
213
|
+
held = []
|
|
214
|
+
subscription.unsubscribe()
|
|
215
|
+
}
|
|
216
|
+
})
|
|
217
|
+
}
|
|
@@ -274,10 +274,51 @@ describe('set', () => {
|
|
|
274
274
|
expect(output).toEqual({a: 1, nonexistent: {path: 999}})
|
|
275
275
|
})
|
|
276
276
|
|
|
277
|
-
|
|
277
|
+
// Content Lake never creates array items when setting: a keyed or indexed
|
|
278
|
+
// segment that does not resolve is a silent no-op server-side. The applier
|
|
279
|
+
// used to append a degenerate item (without the key, even), which
|
|
280
|
+
// fabricated array items locally that would never exist on the server.
|
|
281
|
+
it('does not create an item from a key constraint if the key is not present', () => {
|
|
278
282
|
const input = {items: [{_key: 'item1'}]}
|
|
279
283
|
const output = set(input, {'items[_key=="item2"]': {_key: 'item2'}})
|
|
280
|
-
expect(output).toEqual({items: [{_key: 'item1'}
|
|
284
|
+
expect(output).toEqual({items: [{_key: 'item1'}]})
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it('does not create an item when setting a property under an unresolvable key constraint', () => {
|
|
288
|
+
const input = {
|
|
289
|
+
blocks: [
|
|
290
|
+
{
|
|
291
|
+
_key: 'b1',
|
|
292
|
+
children: [{_key: 's1', _type: 'span', text: 'hello', marks: []}],
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
}
|
|
296
|
+
const output = set(input, {'blocks[_key=="b1"].children[_key=="missing"].marks': ['mark1']})
|
|
297
|
+
expect(output).toEqual(input)
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it('does not create an item when setting through an out-of-bounds index', () => {
|
|
301
|
+
const input = {items: [{_key: 'item1', value: 1}]}
|
|
302
|
+
const output = set(input, {'items[3].value': 99})
|
|
303
|
+
expect(output).toEqual(input)
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
it('still sets a missing property on an existing keyed item', () => {
|
|
307
|
+
const input = {items: [{_key: 'item1'}]}
|
|
308
|
+
const output = set(input, {'items[_key=="item1"].value': 42})
|
|
309
|
+
expect(output).toEqual({items: [{_key: 'item1', value: 42}]})
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it('does not create anything when an array-addressing segment targets a non-array', () => {
|
|
313
|
+
const input = {items: {nested: true}}
|
|
314
|
+
const output = set(input, {'items[_key=="item1"].value': 42})
|
|
315
|
+
expect(output).toEqual(input)
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
it('does not create an item when setting through a negative out-of-bounds index', () => {
|
|
319
|
+
const input = {items: [{_key: 'item1', value: 1}]}
|
|
320
|
+
const output = set(input, {'items[-5].value': 99})
|
|
321
|
+
expect(output).toEqual(input)
|
|
281
322
|
})
|
|
282
323
|
})
|
|
283
324
|
|
|
@@ -299,6 +340,12 @@ describe('setIfMissing', () => {
|
|
|
299
340
|
const output = setIfMissing(input, {'a.b': 42})
|
|
300
341
|
expect(output).toEqual({a: {b: 1}})
|
|
301
342
|
})
|
|
343
|
+
|
|
344
|
+
it('does not create an item under an unresolvable key constraint', () => {
|
|
345
|
+
const input = {items: [{_key: 'item1'}]}
|
|
346
|
+
const output = setIfMissing(input, {'items[_key=="missing"].value': 1})
|
|
347
|
+
expect(output).toEqual(input)
|
|
348
|
+
})
|
|
302
349
|
})
|
|
303
350
|
|
|
304
351
|
describe('unset', () => {
|
|
@@ -518,6 +565,33 @@ describe('diffMatchPatch', () => {
|
|
|
518
565
|
expect(output).toEqual(input)
|
|
519
566
|
})
|
|
520
567
|
|
|
568
|
+
it('applies the patch when its offsets miss a multi-byte boundary', () => {
|
|
569
|
+
// `\u00f8` is two bytes, so this patch's offsets land inside it and the byte
|
|
570
|
+
// walk overshoots. The default is to throw, which escapes the rebase loop in
|
|
571
|
+
// `reducers.ts` and kills the whole outgoing-actions pipeline: the instance
|
|
572
|
+
// then silently stops writing every document. The patch itself is fine and
|
|
573
|
+
// still applies.
|
|
574
|
+
const input = {foo: 'abcdefg\u00f8'}
|
|
575
|
+
const patch = '@@ -9,1 +9,2 @@\n \u00f8\n+!\n'
|
|
576
|
+
|
|
577
|
+
expect(() => diffMatchPatch(input, {foo: patch})).not.toThrow()
|
|
578
|
+
expect(diffMatchPatch(input, {foo: patch})).toEqual({foo: 'abcdefg\u00f8!'})
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
it('still patches the other properties in the same mutation', () => {
|
|
582
|
+
const input = {multibyte: 'abcdefg\u00f8', ascii: 'the quick brown fox'}
|
|
583
|
+
|
|
584
|
+
const output = diffMatchPatch(input, {
|
|
585
|
+
multibyte: '@@ -9,1 +9,2 @@\n \u00f8\n+!\n',
|
|
586
|
+
ascii: '@@ -13,7 +13,7 @@\n own \n-fox\n+cat\n',
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
expect(output).toEqual({
|
|
590
|
+
multibyte: 'abcdefg\u00f8!',
|
|
591
|
+
ascii: 'the quick brown cat',
|
|
592
|
+
})
|
|
593
|
+
})
|
|
594
|
+
|
|
521
595
|
it('applies a diff-match-patch that makes no changes', () => {
|
|
522
596
|
const input = {foo: 'unchanged'}
|
|
523
597
|
const patch = '@@ -1,9 +1,9 @@\n unchanged\n'
|
|
@@ -175,6 +175,54 @@ export const ensureArrayKeysDeep = memoize(<R>(input: R): R => {
|
|
|
175
175
|
return Object.fromEntries(entries) as R
|
|
176
176
|
})
|
|
177
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Whether every array-addressing segment (keyed `[_key=="…"]` or numeric
|
|
180
|
+
* index) in the given match path resolves to an existing array item.
|
|
181
|
+
*
|
|
182
|
+
* This mirrors Content Lake, which auto-creates missing *object* properties
|
|
183
|
+
* when setting but never creates *array items*:
|
|
184
|
+
*
|
|
185
|
+
* ```js
|
|
186
|
+
* // object properties are created, even nested ones:
|
|
187
|
+
* set({}, {'meta.author.name': 'Ada'})
|
|
188
|
+
* // -> {meta: {author: {name: 'Ada'}}} (applies)
|
|
189
|
+
*
|
|
190
|
+
* // array items are not:
|
|
191
|
+
* set({items: [{_key: 'a'}]}, {'items[_key=="b"].title': 'x'})
|
|
192
|
+
* // -> {items: [{_key: 'a'}]} (no-op)
|
|
193
|
+
* ```
|
|
194
|
+
*
|
|
195
|
+
* Without this filter, the second example fabricated a degenerate item
|
|
196
|
+
* (`{title: 'x'}`, no `_key`) that would never exist on the server, which
|
|
197
|
+
* is how concurrent Portable Text edits grew phantom span-less children.
|
|
198
|
+
*/
|
|
199
|
+
function resolvesArraySegments(input: unknown, path: SingleValuePath): boolean {
|
|
200
|
+
let current: unknown = input
|
|
201
|
+
for (const segment of path) {
|
|
202
|
+
if (typeof segment === 'string') {
|
|
203
|
+
// missing object properties (nested ones included) are created
|
|
204
|
+
// server-side, so a string segment may descend into undefined
|
|
205
|
+
current =
|
|
206
|
+
typeof current === 'object' && current !== null && !Array.isArray(current)
|
|
207
|
+
? (current as Record<string, unknown>)[segment]
|
|
208
|
+
: undefined
|
|
209
|
+
continue
|
|
210
|
+
}
|
|
211
|
+
// keyed and numeric segments address array items, which are never created
|
|
212
|
+
if (!Array.isArray(current)) return false
|
|
213
|
+
if (isKeySegment(segment)) {
|
|
214
|
+
const index = getIndexForKey(current, segment._key)
|
|
215
|
+
if (index === undefined) return false
|
|
216
|
+
current = current[index]
|
|
217
|
+
} else {
|
|
218
|
+
const index = segment < 0 ? current.length + segment : segment
|
|
219
|
+
if (!(index in current)) return false
|
|
220
|
+
current = current[index]
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return true
|
|
224
|
+
}
|
|
225
|
+
|
|
178
226
|
/**
|
|
179
227
|
* Given an input object and a record of path expressions to values, this
|
|
180
228
|
* function will set each match with the given value.
|
|
@@ -198,6 +246,7 @@ export function set(input: unknown, pathExpressionValues: Record<string, unknown
|
|
|
198
246
|
replacementValue,
|
|
199
247
|
})),
|
|
200
248
|
)
|
|
249
|
+
.filter(({path}) => resolvesArraySegments(input, path))
|
|
201
250
|
.reduce((acc, {path, replacementValue}) => setDeep(acc, path, replacementValue), input)
|
|
202
251
|
|
|
203
252
|
return ensureArrayKeysDeep(result)
|
|
@@ -231,6 +280,7 @@ export function setIfMissing(
|
|
|
231
280
|
}))
|
|
232
281
|
})
|
|
233
282
|
.filter((matchEntry) => matchEntry.value === null || matchEntry.value === undefined)
|
|
283
|
+
.filter(({path}) => resolvesArraySegments(input, path))
|
|
234
284
|
.reduce((acc, {path, replacementValue}) => setDeep(acc, path, replacementValue), input)
|
|
235
285
|
|
|
236
286
|
return ensureArrayKeysDeep(result)
|
|
@@ -567,7 +617,16 @@ export function diffMatchPatch(
|
|
|
567
617
|
)
|
|
568
618
|
}
|
|
569
619
|
|
|
570
|
-
|
|
620
|
+
// Offsets arrive as UTF-8 byte positions and can overshoot when they
|
|
621
|
+
// land inside a multi-byte character, which happens once the string a
|
|
622
|
+
// patch was computed against has diverged. The default is to throw, and
|
|
623
|
+
// that throw escapes the rebase loop in `reducers.ts`, kills the
|
|
624
|
+
// outgoing-actions pipeline, and leaves the instance silently dropping
|
|
625
|
+
// every write. Clamping to the nearest index applies the patch instead,
|
|
626
|
+
// which is what the Studio and the Portable Text editor already do.
|
|
627
|
+
const [nextValue] = applyPatches(parsePatch(dmp), value, {
|
|
628
|
+
allowExceedingIndices: true,
|
|
629
|
+
})
|
|
571
630
|
return {path, value: nextValue}
|
|
572
631
|
})
|
|
573
632
|
.reduce((acc, {path, value}) => setDeep(acc, path, value), input)
|