Proto commits in yorkie-team/yorkie-ios-sdk

These 57 commits are when the Protocol Buffers files have changed:

Commit:f20a3fe
Author:Lewy
Committer:GitHub

Apply updates from yorkie-js-sdk 0.7.26 (#280) * Correct what an old server does with a presence patch Port of yorkie-team/yorkie-js-sdk#1488 (comments only). * Keep a peer's presence when its watch stream unwatches Port of yorkie-team/yorkie-js-sdk#1501. * Skip a remote change this SDK cannot apply, as Go does Port of yorkie-team/yorkie-js-sdk#1502. * Keep both halves of a combined Tree style on the wire Port of yorkie-team/yorkie-js-sdk#1503. iOS already encoded both fields; the decoder kept only attributesToRemove when it was non-empty. * Qualify Document in DevtoolsUI to build with the Xcode 27 SDK SwiftUI on the Xcode 27 SDK also declares a Document type, so the bare name became ambiguous in YorkieInspectorView and YorkieInspectorModel and the package (and swift test) no longer built. * Keep attributes on the span a reversed insertion records Port of yorkie-team/yorkie-js-sdk#1504. Decoded text restore spans are now rejected at the wire boundary (missing created_at, or a range that does not cover the content) instead of being clamped, matching yorkie-js-sdk and the server. * Keep sync and Watch retrying when a token refresh fails Port of yorkie-team/yorkie-js-sdk#1513. Behavioural change on iOS: authTokenInjector failures were swallowed everywhere (try?). Now only the retry loops (sync loop, Watch streams, broadcast retries) treat a failure as transient and back off (1s doubling to 30s, 20% jitter; a loop's 401 retry waits for it, at most 5s). One-shot calls (attach, detach, remove, sync, the first broadcast try, ...) throw the injector's error, as in JS. deactivate() stops locally and throws the injector's error when the server refuses it with a 401 and the refresh fails. A failed realtime sync restores changeEventReceived, and sync loops are numbered so a deactivate followed by activate cannot leave two running. * Keep Text undo converging through GC with a smaller ledger protocol Port of yorkie-team/yorkie-js-sdk#1514 (yorkie-team/yorkie#2310). Each Text piece keeps the deletions that removed it and the undos that cancelled some of them; an undo cancels only its own actor's removal, and a deletion or redo removes only what its author saw live. GC holds a tombstone until its ledger is stable and a single deletion holds it, and prunes pairs every replica knows. A recreated piece is anchored before the nearest following piece of its insertion, and an empty undo edit runs as a no-op at the head of the text. Snapshots gain TextNode.removal_events (proto regenerated with the pinned buf plugins); a malformed ledger is dropped with a warning. iOS-specific: the pre-attach ticket re-issue walks text node fields by an explicit list (JS walks by reflection), so removal_events is added there too. * Update CHANGELOG for v0.7.26 * Bump yorkieVersion to 0.7.26 * Bump CI yorkie server to v0.7.26 * Fix SwiftLint and SwiftFormat findings in the ledger port * Port tests from yorkie-js-sdk 0.7.26 Text removal ledger (text_removal_ledger, text_undo_removal_ledger, text_undo_fuzz with the adv_sync helper), text restore after GC, LLRBTree ceilingEntry, remote skip, combined Tree style, peer presence across unwatch/watch, token refresh backoff (unit + webhook integration). * Refresh the token on a channel heartbeat or channel sync 401 Review follow-up to the yorkie-js-sdk#1513 port. In yorkie-js-sdk the channel heartbeat runs inside the sync loop, so its 401s go through handleConnectError as a loop; on iOS the heartbeat is its own timer and only logged the error, and syncChannel had no error handling, so the token was never refreshed for them. Also schedule a failed sync loop's retry as one wait of loopRetryDelay, as JS does, instead of sleeping it and then adding reconnectStreamDelay; keep the injector's error when deactivateInternal fails on the local-stop path; keep a retry loop running if handleLoopConnectError ever sees a throw; note why lamport-0 ledger stand-ins are not re-issued. * Test token refresh on channel heartbeats, channel sync and sync loop retry * Correct the CHANGELOG note on malformed restore spans The server has validated restore spans on push since they were introduced (yorkie #1875, v0.7.13), so no stored change carries a malformed one; the earlier note cited an unrelated change. * Address review on PR #280 - Drop a channel heartbeat's error when the client session changed while it was in flight, so it cannot tear down a session started after it was sent. - Drop a sync pass's error when a newer sync loop has started, for the same reason. - Skip a remote dedup increase whose value is not a primitive, as the other remote skips do. - Stop the sync loop at the end of the sync-loop retry test, assert the 401 code where a test expects it, and measure restore-span intervals in UTF-16 units in a test.

The documentation is generated from this commit.

Commit:d45d186
Author:Lewy
Committer:GitHub

Apply updates from yorkie-js-sdk 0.7.25 (#279) * Identify the local user by actor, not by session id Port of yorkie-js-sdk#1461. The RichTextEditor example compared a peer's presence clientID against client.id (the per-session id), which never matches the actor stamped into documents on a v0.7.20+ server, so the local user was listed among its own peers. Compare against getActorID(). * chore: bump CI yorkie server to v0.7.25 * chore: bump yorkieVersion to 0.7.25 * Re-issue pre-attach tickets to the client's actor on attach Ports yorkie-js-sdk ed25355e5 (#1449, yorkie#2111). A document edited before Client.attach minted every ticket under the initial actor, and setActor rewrote only the change IDs and executedAt, so two clients that filled the same key before attaching pushed values with the same createdAt and diverged. On attach, a never-synced document (detached, initial checkpoint, version vector naming only its own actor, nothing absorbed from a snapshot, a remote change or a persisted envelope) re-issues its tickets to the client's actor. Sources/API/Reissue.swift round-trips each operation through the wire format and rewrites every non-zero TimeTicket, nested Object/Array/Tree element bytes included; a hand-written walk because swift-protobuf has no mutable reflection, with every optional message field guarded by its hasX so no presence is set. A Set/Add/ArraySet whose value is a Text is rebuilt from its full element encoding, since the wire drops text content. The root and presences are rebuilt by replaying the re-issued changes on a fresh CRDTRoot, each under the source it ran with, and swapped in atomically with the re-keyed change ids and version vectors; a throw leaves the document untouched. The clone and the undo history are dropped and the devtools recording restarts from a snapshot baseline. Client re-issues only when its key was generated (an explicit key is shared by every session, so a re-issue could mint lamports an earlier session used). A per-client claim per document key, taken once a document has minted operations (at attach, or at detach for one that minted while attached), declines a second never-synced document of the key. Bundled, as upstream: - Change.isUndoRedo / markAsUndoRedo(); applySnapshot and restoreAppendedChanges replay each queued change under its own source. - toTextNodes writes insPrevID only for an already-emitted target, and fromText accepts a link only by exact id, same createdAt and lower offset. - RGATreeSplit.splitNode rejects a negative offset (would otherwise be an NSRangeException on iOS). Gap fixed in the same commit: Converter.toElementSimple never encoded the value bytes of a CRDTObject or CRDTArray, which JS always does. Without them a re-issue would have wiped objects and arrays an undo restored. It also closes the array reorder/undo convergence gap that GCContainmentIntegrationTests had pinned as an expected failure, so that test now asserts convergence as upstream does. toElementSimple now throws (it no longer fatalErrors on a tree that fails to encode). Persistence of the undo mark: Change.isUndoRedo is stored as a local-only swift-protobuf unknown field (number 536000001, far above anything the Change message uses or will grow, outside the reserved 19000...19999 range) on the persisted PbChange bytes only -- Document.toBytes and the StoredChange log, and their decode paths. Converter.toChangePack never carries it, and a DocStore only ever sees opaque bytes, so third-party implementations are unchanged; an older SDK reading it skips the unknown field and replays as .local, as before. Not applicable on iOS: the ReissueToken test (the re-issue entry point, Document.setActor(_:reissue:), is internal, a compile-time guarantee) and the CSPRNG-throw test (the generated key is a UUID, which uses the system CSPRNG). The JS "tombstoned text attribute" snapshot test is already covered by StyleTombstoneTests. Tests: ReissueTests, SnapshotConverterTests, SetActorReissueTests (incl. the seeded mulberry32 fuzz, all 300 seeds), ReissueClaimTests (claim logic; the attach-level scenarios need a server and live in PreAttachIntegrationTests), and a read-only SwiftProtobuf.Visitor ticketsOf helper in Tests/Helper. * Drop a pull already in flight when switching to push-only Port of yorkie-js-sdk#1453. In JS, changeSyncMode runs on a task queue behind a sync that may already be pulling, so the response guard still saw Realtime and applied the pulled changes after a pause was asked for; JS fixes it by counting pending pauses (pendingPullPauses). iOS is structurally immune, so no counter is added (it would be dead state): Client.changeSyncMode is synchronous on the MainActor and sets attachment.syncMode before returning, and syncInternal's drop guard reads attachment.syncMode after awaiting pushPullChanges, so a pause requested mid-flight is already observed. A comment at the guard records this. Regression tests (integration) port "Should drop a pull already in flight when switching to ..." for RealtimePushOnly and RealtimeSyncOff: c1's first pushPull response carrying changes is held, c2 increments a counter, c1 pauses while it is held, the pull is released and must not be applied, and switching back to realtime must catch up. Verified they fail if the guard stops reading the live mode. New internal test seam on YorkieService, active only when mocking is enabled (a plain call otherwise), keyed by the same method spec as setMockError: observeRequests(for:_:) sees outgoing requests and setResponseHook(for:_:) runs an async hook after the real RPC returns and before the caller sees the response. Exposed through Client like setMockError, with unit tests for the seam. Streaming watch is not covered yet. * Report a remote split where it landed, not where it was asked for When a replica receives a Tree split at a boundary where it concurrently inserted text, section 7.3 keeps that insert on the left of the split, but the split's TreeChange reported the index the peer asked for, computed before the split. A binding replaying OpInfos by index moved the insert into the new element and no longer matched the tree. Once a level has split, applySplitLevel measures the end of what stayed in the parent (after the section 7.5 advance) and the first level's index/path become the split change's position; later levels only log a warning when their boundary does not nest. A measurement failure falls back to the requested position. The insert change keeps fromIdx. CRDT state is unchanged; only the reported position moves. Tests: TreeSplitOpInfoTests (11 cases x 3 orders, with the known ancestor-reorder gap asserted to still reproduce, as JS it.fails) and TreeSplitOpInfoIntegrationTests. Ports yorkie-js-sdk a74b25134 (#1455) * Stop the split-sibling cascade at a sibling the editor saw alive Two replicas each splitting <span>de</span> at offset 0 and deleting the empty left piece, which is what an editor does for Enter before a styled run, lost "de" on one replica. The split-sibling cascade walked the whole insNextID chain of the deleted element and tombstoned every split product the editor did not know, including the concurrent product that held the moved text. collectUnknownSplitSiblings now passes a sibling the editor knew only if the editor saw it gone (removed by a change the editor knew) or this delete encloses it whole (start and end tokens in the range, computed lazily once per edit). Any other known sibling ends the walk. Unknown siblings are still collected with their subtrees. Tests: TreeSplitSiblingCascadeTests (10 cases x swap x flip, with GC leaving no garbage) and TreeSplitCascadeRegressionTests (5). Ports yorkie-js-sdk 6f690a147 (#1456) * Recover a document in place after the server refuses its writes When an auth webhook starts denying a client's writes mid-session (a locked document, a read-only downgrade, a revoked link), the refused changes were re-sent and re-denied on every sync, and since push and pull share one RPC the client also stopped receiving peers' changes. Port of the in-place recovery. Public API added: - Client.resync(_:discardLocalChanges:initialPresence:) -> [DroppedChange] re-anchors the same Document on the server state: discards the queue, detaches without a presence clear, re-attaches under the same sync mode, disableGC, pinned poll interval and presence, and restarts the sync loop. A refused detach restores the queue; past the detach the loss is published. - Document.isMintable; update/undo/redo throw errRefused and canUndo/canRedo are false while the discard's clientSeq hole is open. - LocalChangesDroppedValue.Reason.writeDenied ("write-denied"). Behavioural changes: - A PushPull ErrPermissionDenied now publishes auth-error (Watch stays Unauthenticated only). - detach skips the presence clear when the document cannot mint. iOS-specific design: - No task queue, detaching flag or trackSync: resync holds syncSemaphore across quiesce persistence, discard, detach, dropped event, re-anchor and attach, and releases it before restarting the sync loop (a tick takes the same semaphore). - Presence is re-attached from its stored JSON strings through an internal encoded path, not a lossy [String: Any] round trip. - On a refused detach, offline persistence is reinstalled. - A real server acks the attach-time presence change, so the dropped-change list holds 1 change where the JS test's fake expects 2. Ports yorkie-js-sdk 61ca06c93 (#1463) * Stop retrying a push the server will never accept Port of the yorkie-js-sdk change. Since server v0.7.24 a push over the document size limit comes back as ResourceExhausted carrying ErrDocumentSizeExceedsLimit, and a single oversized change as ErrChangeTooLarge. handleConnectError retried every ResourceExhausted, so the sync loop resent the same pack for as long as the document stayed attached, and the shared PushPull RPC also stopped pulling other peers' changes. Public API added: DocEventType.writeRejected ("write-rejected"), WriteRejectedValue (code: .documentSizeExceedsLimit / .changeTooLarge, reason, method), WriteRejectedEvent, Document.subscribeWriteRejected and unsubscribeWriteRejected. The event is also delivered to the default subscribe callback. YorkieError.Code.errChangeTooLarge is new. Devtools ignores the event through its existing default. Behavioural change: handleConnectError no longer retries these two codes (checked ahead of the generic ResourceExhausted branch; a ResourceExhausted without a Yorkie code is still retried). syncInternal publishes WriteRejectedEvent next to syncFailed. The rejected document is parked on its Attachment and skipped by the realtime sync loop until a push is accepted (an explicit sync(doc) resends and clears the mark) or Client.resync(discardLocalChanges:) recovers it. Detach, remove and attach still send a refused pack as-is. iOS note: the realtime loop iterates attachments sequentially rather than in parallel, so the rejection is caught per attachment (syncParkingWriteRejection) and swallowed there, keeping the loop and the other documents alive. Tests use a new internal, mocking-gated setMockError(for:error:where:) seam to refuse one document's push. Ports yorkie-js-sdk 29a81fe01 (#1462) * Reconnect a Watch stream that goes silent past the server heartbeat A quiet document sends nothing on its Watch stream, so a half-open connection (resume from sleep, network change, a proxy dropping an idle socket without a reset) looked exactly like a quiet document: the stream stayed "connected" while the document froze. Server 0.7.25 can heartbeat idle streams and advertises the interval in the init response. When it is non-zero, the client now treats three intervals of silence (clamped to [1s, 24h], as in the Go client) as half-open: it abandons the stream, logs ErrWatchStreamIdle, publishes the same reset as the error path for documents (online clients cleared, Initialized, disconnected) and goes through the existing reconnect path. An interval of 0, which older servers send, keeps today's behavior. - WatchIdleWatchdog (Core/Watch.swift): one live task, re-armed for the remaining time while activity keeps arriving, armed only after the init response; now/sleep are injectable for deterministic tests. - Every watch response (init, event, heartbeat) records activity; a heartbeat body is otherwise ignored by the document and channel handlers. The watchdog is cancelled with the stream. - A stream that was cancelled by the client is never reported as idle. Proto sync (resources.proto, yorkie.proto, regenerated with the pinned buf plugins): PresenceChange CHANGE_TYPE_PATCH = 4 and removed_keys, WatchResponse.heartbeat, WatchInitialization.heartbeat_interval_ms and message WatchHeartbeat. The deprecated WatchDocument/WatchChannel rpcs stay absent. Converter does not handle PATCH yet. Generation guard: each attachment carries a watch stream generation that is bumped when a stream starts or is abandoned as idle. A stream ignores its own later messages and completion once stale, so an abandoned stream (whose read on a half-open socket may never settle, or settle later with an error) can neither reach onStreamDisconnect/handleConnectError nor cancel the successor stream. Ports yorkie-js-sdk c69e6bdaf (#1472) * Do not send the session id as the Watch actor The Watch request carried `actorID = self.actorID ?? id`, falling back to the session id when no stable actor was known. yorkie-js-sdk sends `actorID ?? ''` and never the session id, which the server rejects as a mismatch; an empty value makes the server fall back to the session id itself, as old SDKs get by omitting the field. This was a pre-existing divergence from yorkie-js-sdk (document and channel requests alike). Both requests now send the stable actor or an empty string, restoring parity. The request construction moves into makeDocumentWatchRequest and makeChannelWatchRequest so it can be unit tested. * Send presence as a patch when the server advertises presence-patch presence.set() always recorded a full PUT, so a presence holding one large key was re-uploaded whole on every cursor move. Since yorkie#2164 the server accepts a patch carrying only the changed top-level keys and folds it into a full PUT before storing, so pulls are unchanged and only the upload path shrinks. Presence.set now records a patch while the server advertised presence-patch on the pack it last returned (ChangePack.capabilities) and holds a base of ours. Every way that base can go missing falls back to a full PUT: no capability, the capability dropped mid-session (heal with one PUT, also while later patches are still queued, also on the push-only ack path), attach, clear(), restore or re-anchor, a presence over the server's 4 KiB base limit, and ErrPresenceBaseUnavailable, which rewrites the earliest pending patch into the PUT it stands for and resends once. The retry covers every push path: attach, re-anchor attach, sync, detach and remove. Only the changed keys go on the wire; the local-only `full` copy and removed_keys are never sent. A PATCH decoded off the wire is merged onto the held presence. Persistence divergence from JS: iOS persists protobuf changes, which cannot carry a patch's local-only `full`, so a pending patch is written as the PUT of its full presence (toBytes envelope and the append-only change log). No patch is pending after a restore. Not applicable: the JS fallbacks for an update or undo that removes a presence key. iOS presence cannot remove a key and has no presence undo. Requires yorkie server v0.7.25+ to take patches; an older server advertises no capability and keeps receiving full PUTs. Ports yorkie-js-sdk b50f89006 (#1477) * Refuse a merge that would move a node under itself The merge step of CRDTTree.edit resolves its destination through mergedInto, which rebuildMergeState derives from the mergedFrom an element payload keeps. A crafted lineage can aim that destination at a child the merge moves, or at a node inside one. moveChild then re-parents the child under itself and updateAncestorsSize loops forever. applyMergeMoves now refuses such a move as the server does: the child is tombstoned in place with its subtree (tombstoneSubtree), a GC pair is booked for each node before the merge-delete propagation, every later child of the same merge is stranded too, and none of them is stamped mergedFrom or moved. edit/editT return the stranded nodes as a new last tuple element, and TreeEditOperation counts them in the removed size. Tests: the three upstream cases (destination is the moved node, destination inside it, children after a refused one) in CRDTTreeMergeLineageTests. The edit runs on a worker with a 10s wait so a regression fails instead of hanging the run. Destructuring sites in Sources/Tests and the CopyPathForcingCRDTTree override are updated for the longer tuple. Ports yorkie-js-sdk 4d7140689 (#1478) * Resolve the index before a text that follows an element CRDTTreePos.fromTreePos anchored a text reached at offset 0 on the text itself unless it was the first visible child. A left sibling means "after it", so in <p><b>hello</b> world</p> index 8 inserted after " world", and edit(8, 14) deleted nothing. A text at offset 0 now anchors on its previous visible sibling, or on the parent when it is the first visible child. Only local index conversion changes; convergence and the wire format are unchanged, but the position goes on the wire so every SDK must resolve an index the same way. Tests: CRDTTreeElementTextBoundaryTests ports tree_element_text_boundary_test.ts (boundary edits, concurrent split, concurrent edit over the converter). The full unit suite passes unchanged: no existing test encoded the old anchor. Ports yorkie-js-sdk 8ad8ce77f (#1482) * Skip a Tree restore span with a text parent A Tree restore span whose parent ID names a text node threw "Text node cannot have children" on apply. The server stores a change before applying it, so one such change wedged the document on every replica that pulled or replayed it. Restore now applies the three rules the Go SDK merged in yorkie#2175, so every SDK skips the same spans: - restoreKindAgrees: restore and retombstone skip a span whose isText disagrees with its node type. - recreateParent: a recreate is left unplaced when the parent is missing, is a text node, or when a surviving node already holds the identity, exactly or inside a text run of the same insertion. - recreateFromSpan computes the identity first and builds text nodes with it. All are apply-time skips, so stored changes stay loadable. The iOS-only bounds guard on the text slice stays, after recreateParent. Tests: TreeRestoreSpanKindTests (kind mismatch, claimed identity) and TreeRestoreTextParentTests (crafted restore operations swapped into a change, converter round trip, pull vs full replay, honest undo after GC). Ports yorkie-js-sdk 5c8dd738c (#1475) * Ask the server for Watch heartbeats at a chosen interval The server now heartbeats a Watch stream only when the stream asks, so the idle watchdog never armed against a server that gates heartbeats on the request. Both the document and the channel Watch request now carry ClientOptions.watchHeartbeatInterval, 20 seconds by default. Zero or a negative value asks for none, and a value past 2^53 - 1 is capped as yorkie-js-sdk does, so the same option asks the same of the server on every SDK. The conversion runs once in Client init. Option type and unit: watchHeartbeatInterval is an Int in milliseconds, matching the neighbouring options (syncLoopDuration, reconnectStreamDelay, attachTimeout), and the JS option, so a value ports unchanged. It is an optional init parameter that defaults, so the change is additive and source-compatible. Because it is a whole number, JS's NaN and fractional round-up cases have no counterpart. Proto: WatchRequest.heartbeat_interval_ms = 4 (int64), regenerated with the pinned buf plugins; the generated diff is the new field only. Adds an internal, mocking-gated YorkieService.observeWatchResponses seam so the integration tests can read the init response and heartbeats the client otherwise consumes. Those cases skip on a server that predates the per-stream heartbeat. Ports yorkie-js-sdk c1a03877e (#1483) * chore: add CHANGELOG entry for v0.7.25 * Close the Watch stream generation guard while a failed stream is handled The Watch result closures ran in a plain Task, so the generation check, the activity record, the message handler and onStreamDisconnect were separate main-actor hops. In a completion, the idle watchdog was only cancelled inside onStreamDisconnect, after `await handleConnectError(error)`, which can suspend on an auth token refresh. If the watchdog fired during that await it bumped the generation and reconnected, and the resumed old completion then disconnected the successor stream. yorkie-js-sdk v0.7.25 clears the idle timer first thing in its catch, because the stream is over and the timer must not fire during the await that follows. Do the same: - Move the document and channel result handling into handleDocumentWatchResult / handleChannelWatchResult, run in one `Task { @MainActor in }` hop so the check and the synchronous work it guards cannot be split. The message handler is now synchronous. - Cancel and clear the attachment's watchdog before any await in both `.complete` branches. - Re-check the generation after handleConnectError and return when stale, before publishing the auth error or disconnecting. - Log a failed onStreamDisconnect instead of discarding it with try?, and point the DocC link at the current makeDocumentWatchRequest signature. Add Client-level watchdog tests on a real Client (mocking enabled) with an injected clock (Client.watchIdleClock) and a test adoptAttachment seam: arming on init only, idle fire order and reconnect, stale completion and message after a bump, a slow handleConnectError with and without a replaced stream (document and channel), and re-arming on the reconnected stream. * Heal a restored presence patch the server may have stored as empty Persistence writes a pending presence patch as the put of its `full` presence (a user-approved divergence from yorkie-js-sdk, whose JSON ChangeStruct keeps the patch). That left a hole: a patch pushed to a server without the presence-patch capability is stored as an empty presence, and if the reply is lost and the app restarts, the restored queue holds a put at the same clientSeq. The server deduplicates it, no heal is owed because the restored queue holds no patch, and peers see an empty presence forever. In yorkie-js-sdk v0.7.25 the restored patch keeps patchSent true, so the first reply without the capability heals ("heals a restored patch the server stored as empty"). Keep the put on disk and also persist a local-only marker that the change was minted as a patch: an unknown field on the persisted PbChange, stored exactly like the undo/redo mark (persistedPresencePatchFieldNumber = 536_000_002, next to 536_000_001; an older reader skips it and restores a plain put). Converter exposes it through fromPersistedChanges as Change.wasMintedAsPresencePatch, which the re-issue path carries over and a second persist writes again, so it survives any number of restarts. hasPendingPresencePatch counts a marked change, so resetPresenceStateForRestore leaves patchSent true and the next reply without the capability heals. toPersistedChange and toPersistedChanges now throw instead of swallowing a failed merge with try?; their callers already throw. Tests: the "does not heal a restored put" case is replaced by the heal case, plus a put that was never a patch (no heal), the append-only log path, a second restart, a server that takes patches, and converter round trips of both marks. * Do not send an actor on the channel Watch request yorkie-js-sdk v0.7.25 createChannelWatchStream sends only the client id, the heartbeat interval and the channel resource; a channel session is not a document actor. The iOS channel request declared the client's stable actor anyway, which a server that checks the declared actor against the session could read as a mismatch. Drop the actor from makeChannelWatchRequest (static and instance). The document Watch request keeps `actorID ?? ""` as upstream does. * Read the resync target under the sync lock The attachment and ReattachOptions were captured before waiting for the sync semaphore, so a resync queued behind an in-flight sync or another resync acted on a stale attachment and could undo a sync mode change made while it waited. Both are now read right after the wait, releasing the semaphore when the document is gone. * Leave persistence torn down after a refused resync detach, as JS does A refused detach no longer reinstalls offline persistence, matching yorkie-js-sdk. The document is still restored to its attached state with the queue intact. * Keep syncing the other documents when one explicit sync fails An explicit client.sync() over all documents stopped at the first error. It now tries every attachment and rethrows the first error afterwards, as the JS Promise.all still pushes the rest. * Guard the YorkieService test seams with a lock The mock error, observer, hook and predicate dictionaries were mutated by tests on the main actor and read from nonisolated RPC methods. They now sit behind an NSLock (OSAllocatedUnfairLock needs iOS 16, the package targets 15); every access stays behind isMockingEnabled so a client without mocking never takes it. * Harden silent failures found in review Build the change pack once per push, log operations Converter.toOperations cannot encode, record a TicketReissuer failure for an actor with no byte form instead of rewriting onto an empty one, warn when the devtools baseline fails to encode, and document that the reissue test fixture must follow resources.proto. * Tighten the Tree merge-lineage and split OpInfo tests A merge-lineage edit that threw instead of hanging passed silently because editWithTimeout swallowed the error; it now fails the test. The cycle-guard fixtures tombstone through remove() like the JS counterparts, and the split OpInfo known-gap convergence check is documented as deliberately strict. * Push a copy of an appended Text, Counter or Tree, not the handle's element JSONArray.insertAfterInternal inserted a deep copy of the appended Text, Counter, DedupCounter or Tree but pushed the original as the AddOperation payload. The user's handle is bound to that original, so edits made through it in the same update also landed in the payload: a replica built the element from the already-edited payload and replayed the edit on top of it. Appending a JSONText and editing it gave a duplicated value locally and a different one on a peer; JSONCounter(value: 1) then increase(5) gave 11 instead of 6. This is a pre-existing iOS-only divergence; yorkie-js-sdk's ArrayProxy.insertAfterInternal inserts the built element and pushes element.deepcopy(). Do the same, as JSONObject.set already does: insert the element the handle is bound to and push a copy. * chore: list the review fixes in the v0.7.25 CHANGELOG * Deactivate both clients in test_removed_document_creation, as JS does The test detached each document through the client that did not attach it (c1.detach(d2), c2.detach(d3)) and deactivated c1 twice. A v0.7.25 server checks that the pushed actor matches the authenticated client and refuses that detach with PermissionDenied ("actor id does not match the authenticated client"); it failed identically on main. The JS test ends with c1.deactivate() and c2.deactivate() and no detach, so mirror it. * Latch the held pull in the push-only drop tests The response hook fulfilled pullInFlight before storing its continuation, so the test body could resume and release a gate that was still nil, leaving the pull held until pullHandled timed out. The hook also mutated its flags off the main actor. Share the state through a lock-guarded LockedValue and a TestGate latch that remembers an early open(). * Complete the Apache license header in the files this sync adds

Commit:915dd77
Author:Lewy
Committer:GitHub

Apply updates from yorkie-js-sdk 0.7.24 (#278) * Order concurrent splits of one boundary by ticket Port of yorkie-js-sdk#1375. applySplitLevel routes the split point through orderSameBoundarySplit, so concurrent splits of one boundary resolve in ticket order on every replica; the §7.5 advance stops in front of a run of empty unknown splits that reaches the actor, and removeStyle now runs the advance on both range anchors as style does (the advanceSplitSiblings switch in resolveStyleRange is gone). Every insNextID chain walk goes through an InsNextWalker cycle guard. Split links are stripped from decoded element values and edit contents (new SplitLinks.swift, Converter.fromElementSimple / fromTreeNodesWhenEdit), and from the values an undo or redo re-inserts in Document.executeUndoRedo, so the replica that ran the undo agrees with the ones that decoded it. iOS-specific: the new ordering helpers live in a same-file CRDTTree extension to keep the type body under SwiftLint's limit. * Cover concurrent same-boundary splits and split-link stripping The two JS cases marked skipIf(known) -- an older actor splitting the paragraph -- still diverge upstream and are not ported. * Make docSize agree with a rebuild (issue #1383, part 3) Port of yorkie-js-sdk#1392. A removed RHT node is charged for its key only (RHT.remove reports the dropped value via RHTRemoval, and text/tree removeStyle take it out of live or gc); an array move books its movedAt ticket once, to live or -- for a concurrently removed element -- to its gc charge (RGATreeList.moveAfter returns RGATreeListMove, CRDTRoot/ChangeContext.accMovedElement); and Converter.fromTree restores the tree's movedAt/removedAt, which it had been dropping. iOS-specific: CRDTArray.getByID is an unfiltered lookup (get(createdAt:) throws on a removed element); applyValueDropped is a shared free function to keep CRDTTree's type body under SwiftLint's limit. The DocumentSizeTest removeStyle gc expectation moves from {36, 168} to {28, 168}, as upstream. * Cover docSize agreeing with a rebuild * Fix clientSeq rewind, leaked sizeInGC record, tombstone removedAt Port of yorkie-js-sdk#1395, the fix for three defects reported from the iOS 0.7.22 port (yorkie-js-sdk#1377). 1. The log-discontinuity repair carries the header's acked checkpoint across the re-restore (Document.advanceClientSeqTo, forward only) and persists the re-serialized document, so it no longer re-mints clientSeqs the server already took. 2. A released tombstone's zero sizeInGC record no longer pins its subtree. 3. ElementRHT.set leaves an already-removed LWW loser's removedAt alone. iOS-specific for (2): JS switched to a WeakMap. GCChargeKey now holds the element weak (NSMapTable's Swift overlay retained the key across lookups), and a key whose element has gone equals no other key, so a new element reusing the address cannot read the dead record; garbageCollect drops dead records. Three RestoreLogValidationTests assertions move from the raw snapshot bytes to the decoded content and advanced clientSeq. * Cover the clientSeq repair and tombstone removedAt fixes * Guard empty-text anchors and reset the clone on failed applies Port of yorkie-js-sdk#1394 (the two items that survived its rescope). leftAnchorID anchors an empty text node on its own id instead of offset - 1, which the server rejects. applyChanges and executeUndoRedo now drop the clone when a change or undo/redo throws partway, so the clone never keeps a prefix the root did not take; both split into an outer try and an *Internal body, as in JS. Document.update already reset the clone on an updater throw (0.7.21). emptyRunReachesActor already read the unfiltered children, so its regression-lock test is ported with no source change. leftAnchorIDForTest, emptyRunReachesActorForTest and pushUndoForTest are test hooks; JS's tests reach the private members through casts and patch SetOperation's prototype, which Swift cannot. * Cover empty-text anchors and clone reset on failed applies * Fix losing ElementRHT values against tombstones Port of yorkie-js-sdk#1398. ElementRHT.set's losing branch required BOTH the occupant and the incoming value to be live before marking the loser removed. When the occupant was already a tombstone, that gate meant neither branch ran for a losing live value: it stayed live in nodeMapByCreatedAt, never installed under the key and never collected, so key-iteration (which skips removed nodes) and get(key:) permanently disagreed with a replica that took the winning path first -- the exact divergence flagged as yorkie-js-sdk#1376 in the existing iOS comment. Dropped the `node!.isRemoved == false` condition so a live loser is always tombstoned, matching the JS fix exactly. The separate `value.isRemoved == false` gate (yorkie-js-sdk#1377) is unchanged. No iOS-specific divergence; this is a straight port of the JS hunk. * Cover the tombstoned-occupant LWW loser fix Port of yorkie-js-sdk#1398's regression test. Mirrors element_rht_test.ts's "should remove a losing value when the occupant is a tombstone": set a winner, delete it (occupant becomes a tombstone), then set a losing value under the same key. Asserts the loser is marked removed, the key reports absent via has(key:), and the owning CRDTObject's key/JSON views are empty -- all of which regressed to a live, uncollectable loser before the fix. * Drop snapshots in push-only mode but keep the push ack Port of yorkie-js-sdk#1401. An explicit sync(doc) always pulls, and the push-only guard in syncInternal only checked responsePack.hasChanges(), so a snapshot-only pack still replaced the root of a document in .realtimePushOnly or .realtimeSyncOff. Drop those packs too by also checking hasSnapshot(). A dropped pack still carries the client seq ack. Skipping it entirely (the old early `return doc`) meant the confirmed changes stayed in localChanges and were re-pushed, which the server's snapshot build would then apply a second time; it also meant the heartbeat, the offline-persist hook and the synced event never fired for a dropped response. Added Document.acknowledgePushedChanges(_:), mirroring Document.acknowledgePushedChanges in JS: it removes the acked local changes, forwards only the checkpoint's client seq (server seq stays put so the dropped state is pulled again once realtime sync resumes), adopts the pack's compaction epoch, and applies a removal flag. syncInternal now always calls either applyChangePack or acknowledgePushedChanges and runs the heartbeat/persist/synced-event tail either way, deciding the persist branch (full re-snapshot vs. header-only) off a `movedRoot` flag instead of the response shape directly. iOS divergence: JS's persist-ack path additionally inlines a `decide()` closure to pick between a header write and a repair-snapshot once queued writes settle, because `poisoned` can flip while an async store write is in flight. iOS already factors that same repair-on-poisoned-log decision into enqueueMetaPersist (read at write time, after the per-key write chain), so no equivalent closure was needed here -- the existing enqueueMetaPersist/enqueueSnapshotPersist split already covers the dropped-pack case once `movedRoot` is false. * Cover the push-only snapshot drop Port of yorkie-js-sdk#1401's regression test. Mirrors client_test.ts's "Should not apply a snapshot pulled while in push-only mode": c2 pushes enough changes past the snapshot threshold that c1's next explicit sync(doc) is answered with a snapshot instead of changes, while c1 is in .realtimePushOnly. Asserts the snapshot event never fires, the document's own pending edit still lands locally, the push is acked (hasLocalChanges() is false, so it is not re-pushed), the edit still reached the server, and switching back to .realtime lets c1 catch up to what it dropped. * Name the change and operation that cannot be applied Port of yorkie-js-sdk#1403. The checkpoint only advances after a pack's changes have been applied, so a change that throws leaves it where it was: the server redelivers the same pack on every sync and the client throws on the same bytes forever. The only signal was whatever the failing operation happened to throw, with nothing naming the document, the change or the operation, so the wedge looked like a network problem. Add YorkieError.Code.errChangeApplyFailed. Change.execute now names the operation that failed (its type and target ticket, never its `toTestString`, which embeds the operation's payload) and the change id, since that is the only place they are known. Document.applyChange adds the document key as the error passes through, keeping the original error description inline. Document.applyChangePack logs one line naming the checkpoint the document is stuck at before rethrowing. Both wraps are gated on `source == .remote`: `Document.update` and undo/redo call `Change.execute` directly and have a caller waiting on the original error code (`errInvalidArgument`, `errInvalidType`, ...), so only the pull path -- the one with the redelivery problem -- is named. This is the "fail loudly and stop" option of yorkie-js-sdk#1367/#1403, which the upstream issue notes is worth doing whichever later policy (quarantine, bounded retry) is chosen. The loop still wedges on an unapplicable change; that is unchanged. iOS divergences: - `YorkieError` is a flat `{code, message}` struct, not a class, and is internal (not part of the public API: Client/Document's public `throws` surface exposes only the existential `Error`, so this is not a public API addition). There is no iOS counterpart to `ChangeApplyError extends YorkieError` with structured `docKey`/`changeID`/`opIndex`/`operation`/ `cause` fields and a `withDocKey` method; the same metadata is instead composed into the message string across the same two layers upstream uses (`Change.execute` names the operation, `Document.applyChange` adds the document key). - JS additionally excludes `ChangeApplyError` from the offline-restore handler's `actor-mismatch` classification, because it classified *any* `YorkieError` as actor-mismatch. iOS's equivalent (`prepareOfflineResume`) already classifies by the specific `.errActorMismatch` code rather than by type ("classified by error code, not by error type" per its existing comment), so `.errChangeApplyFailed` was already excluded by construction; no change was needed there. - JS also starts dropping the clone when `change.execute` throws inside `update()`'s local-edit path. iOS's `update()` already drops the clone on any updater failure, but the specific local `change.execute` call it makes afterwards uses `try?` and silently discards that error entirely (pre-existing, unrelated to this change) -- left untouched as out of scope for this port. * Cover naming the change and operation that cannot be applied Port of yorkie-js-sdk#1403's regression tests (change_apply_error_test.ts). Uses a SetOperation targeting a parentCreatedAt never registered in any document's root as a deterministic, real fault (iOS has no production-code mocking seam to stand in for vi.spyOn). Covers: the wrapped error names the document, the change id and the operation (type + target ticket, operation index) on a remote apply failure; the operation's own payload (toTestString) never leaks into the message; a local or undo/redo replay of the same failing change is left with its original errInvalidArgument untouched; and applyChangePack leaves the checkpoint stuck (never advances) when the pack it was given cannot be applied. * Warn when offline persistence is used without a client key Port of yorkie-js-sdk#1402. ClientOptions.key defaults to a fresh UUID per Client, and the server derives the actor stamped into every change from it. That is harmless on its own, and stops being harmless the moment ClientOptions.store is set: the next launch gets a different key, hence a different actor and a different store namespace, so it restores nothing and silently strands every un-pushed offline edit from the previous launch -- for an app that looks correctly configured, with no actor-mismatch event to explain it (the old entries are never reached under the new key). Neither option is wrong alone, so neither had reason to complain. Warn where they meet, in Client.init, via the existing Logger (no print()). Documented the same requirement on ClientOptions.key and .store: an opaque random value the app mints once and persists across launches, not a user id, an email, or a device id, since the key is an unauthenticated identifier sent verbatim to the server. Also ported this commit's second, independently-applicable fix: storeKey/sessionLockName joined apiKey/clientKey/docKey with a bare "/", none of which is validated to exclude the separator, so distinct identities could collide on one store namespace or session lock. Added escapeNamespacePart/namespaceOf, percent-encoding "%" and "/" per component, matching upstream's fix in client.ts. iOS divergence: upstream's third fix (util/uuid.ts) replaces a Math.random()-based hand-rolled UUID with one drawn from a CSPRNG (Web Crypto). iOS's default key already comes from Foundation's UUID(), which is backed by the platform's secure random generator -- there is no weak-PRNG problem to fix, so nothing was changed here. * Cover the client-key warning and namespace escaping Port of yorkie-js-sdk#1402's regression tests (client_options_test.ts). iOS's Logger has no test-capture seam equivalent to spying on console.warn, so the warning text itself is not asserted. Instead covers the behaviour it exists to flag: a store configured without a key still gets a usable generated key (the warning is a diagnostic, not a hard failure), and an explicitly supplied key is used as-is. Also covers the namespace-escaping fix directly: two distinct (clientKey, docKey) pairs that would join to the same bare string now produce different storeKey/ sessionLockName values, while ordinary keys with no separator keep the exact pre-existing bare-join format. * Cover reading a double without writing to the buffer it shares Port of yorkie-js-sdk#1422. JS's `Primitive.valueFromBytes` copied a double's payload to offset 0 of the *underlying* `ArrayBuffer` and read it back from there, writing through to whatever else shared that buffer — a snapshot decodes as one buffer handed out as views, so reading a double out of a document could overwrite another field's bytes (observed as a later decode failing with "illegal tag"). The fix widened to the other decoders that trust a peer's payload length (Long/Date/Long-counter readers, HLL.restore). iOS's `Converter.valueFrom(_:data:)` cannot have the shared-buffer defect: it only reads via `withUnsafeBytes { $0.load(as:) }`, never `withUnsafeMutableBytes`, so nothing is ever written back. And unlike JS's `bytes.buffer`, which discards a typed array's `byteOffset` and always points at the start of the underlying buffer, `Data.withUnsafeBytes` is always relative to that `Data` value's own window, even when the `Data` is a slice of a larger shared buffer — so the wrong-offset half of the bug has no counterpart either. `CRDTCounter.restoreHLL` already copies its input into a fresh `[UInt8]` before touching `HLL.restore`, so the HLL path is immune the same way. No source change is ported — only tests pinning the round-trip behaviour that makes iOS immune: - decoding a double out of a slice of a larger shared buffer leaves the rest of that buffer untouched - decoding a double payload longer than 8 bytes reads only the first 8 - a decoded bytes value is independent of the buffer it was read from (copy-on-write) Not ported: the JS tests for payloads *shorter* than 8 bytes (reading missing bytes as zero instead of throwing) and for HLL refusing a malformed-but-right-length register payload. `UnsafeRawBufferPointer .load(as:)` traps when the source is shorter than the type's stride, so a truncated Double/Long/Date/Long-counter payload crashes on iOS rather than reading as zero-padded — a real, separate gap from the shared-buffer bug this commit fixes. Flagging it here rather than fixing it, since a fix is a behavior change outside this port's scope. * Port three tree convergence fixes from the Go SDK Port of yorkie-js-sdk#1404. The server builds snapshots with Go semantics, and three Go fixes had no JS (or iOS) counterpart, so an iOS client and the server, or two iOS clients, could end up with different documents for the same changes. Undo and redo now issue one ticket per split level for a split reverse and record them on the operation (TreeEditOperation.setSplitTickets), the way Go's executeUndoRedo does. Rebuilding them from the operation's own ticket landed on the next operation's ticket in the same undo entry, leaving two live nodes under one id on every replica. `Operation.setActor` is now a protocol requirement rather than an extension-only method: `Change.setActor` calls it through an `Operation` existential, which dispatches statically to the extension default for anything not in the requirement list, so `TreeEditOperation` can now override it to also re-stamp its split tickets with the change's actor. Merge-delete propagation (`CRDTTree.propagateDeletesToMergedChildren`) now skips a merge-source's moved children only when the edit's own positions declared that source as a boundary (`declaredBoundaries`, yorkie#2042) — not whenever the source's `mergedInto` happens to equal this edit's own merge destination. Fixes yorkie-js-sdk#1331. A style's reached set now comes from the change's own positions rather than from whatever the tree currently looks like (yorkie#2038, design doc section 9 "Port specification"). `CRDTTree.style`/`removeStyle` now share one `styleTargets` resolution, built from `DeclaredLineage` (split-lineage closure over a position's declared parent and its ancestors), `boundaryElements` (the elements the change's own positions would have reached through a single token), and the existing §9.4 merged-anchor machinery from the #1375 port. A split family reached only through an End token is now styled only if the range began inside it or at or before it in document order (§9.2 Fix 27, yorkie#2070). `styleSkipPredicate` gained a §9.6 range-start guard: a style range that begins inside an element reaches it only via that element's End token, and the change reached it that way only if the change's own range-start was declared inside it. The decoder (`Converter.fromTreeNodesWhenEdit`) now rejects tree-edit content whose entry decodes to no node, and drops both split-sibling links and merge stamps (`CRDTTreeNode.dropMergeStamps`, new) from edit content — a peer-stamped `mergedFrom` on fresh content could otherwise steer a declared-boundary walk into an element the position never named. `Converter.fromTreeNodes` now rejects a node whose depth names no parent, or a text node as a parent, instead of silently dropping the node via optional chaining. `CRDTTree.fromTreeNodes`'s parent-depth guard is the iOS counterpart of the same hardening. iOS already read split tickets back as sent (`TreeEditOperation.setSplitTickets` via the converter, from the earlier #1319/#1375 ports); that part needed no change. Divergences from the JS source: - `TreeEditOperation.setActor` is a `final class` method, not a `mutating func` on a value type; Swift satisfies a protocol's `mutating func` requirement with an ordinary method on a class. - Node identity sets use `Set<ObjectIdentifier>` throughout (`declaredBoundaries`, `DeclaredLineage`, `boundaryElements`), matching every other such set in this file — `CRDTTreeNode` is a reference type with no value-equality conformance, unlike JS's `Set<CRDTTreeNode>`. - The new style-targeting helpers (`DeclaredLineage`, `declaredLineageOf`, `declaredParentOf`, `splitOriginsOf`, `declaredAncestryHas`, `beginsAtOrBefore`, `unknownSplitSiblings`, `splitFamilyOf`, `boundaryRangeCovers`, `boundaryElements`, `styleSkipPredicate`, `styleTargets`, `styleChangeRange`) live in a dedicated `private extension CRDTTree`, and `findMergeNode`/`declaredBoundaries`/`collectUnknownSplitSiblings`/ `propagateDeletesToMergedChildren` moved into another, to keep the main class body under SwiftLint's 1000-line type-length budget — pure file organisation, no behavioural difference from JS. - `boundaryRangeCovers` and `propagateDeletesToMergedChildren` take a `TreePosRange` tuple (or bundle their parent/left pair) instead of the JS signature's flat positional arguments, to stay within SwiftLint's 5-parameter budget. - No proto/wire changes: no new field is involved in this commit, and `Converter.fromTreeNodesWhenEdit` only gained a `throws` it already needed a caller-side `try` for. * Cover the three tree convergence fixes Covers yorkie-js-sdk#1404 (and partially Go's TestTreeSplitUndo / TestTreeUnwrapAndMergeDelete / tree_style_reached_set_test.go, which this change ports the behaviour of without porting their harnesses). TreeEditOperationSetActorTests pins the dispatch bug the Operation protocol-requirement change fixes: it calls `setActor` through a `Yorkie.Operation` existential — the same way `Change.setActor` holds its operations — and checks that both `executedAt` and every split ticket land on the new actor. Calling the concrete `TreeEditOperation` directly would have passed even with the static-dispatch bug still in place, so the test deliberately goes through the existential. TreeSplitTicketUndoRedoTests reproduces Go's TestTreeSplitUndo locally (no server): a three-paragraph document, two L2 merges in one `update` block, then undo/redo/undo. Without the Document.swift fix this duplicates a node id after the first undo; confirmed by reverting the fix and re-running. TreeMergeConvergenceTests gained one two-client scenario mirroring the JS PR's own new integration test ("Can converge an unwrap with a concurrent delete of the same paragraph"): an unwrap and a concurrent whole-paragraph delete that also unwraps the next paragraph, both landing on the same merge destination. Without the declaredBoundaries fix, one replica keeps the unwrapped paragraph's text after sync instead of converging to drop it; confirmed by reverting the fix (temporarily inlining the old unconditional skip) and re-running. TreeStyleReachedSetConvergenceTests ports 3 of the ~20 cases from the new (929-line) tree_style_reached_set_test.ts as two-client convergence scenarios instead of its single-replica hand-built-batch-replay harness, using the two-client pattern TreeMergeConvergenceTests.swift already established: a concurrent split carrying a style onto both halves, a concurrent merge carrying a style onto both joined paragraphs, and the §9.6 guard not styling a merge target a range-start merely resolved into. Disabling the unknown-split-sibling carry-over and the split-family End-token carry in styleTargets (temporarily, to verify) reproduces the first case's failure ("only one split half gets bolded"); the fix makes all three converge to the exact XML yorkie-js-sdk's test asserts. Not ported: split_ticket_test.ts's wire round-trip and "applies such an undo entry on a replica across the wire" sub-tests (split-ticket wire round-tripping is already covered by TreeEditSplitTicketsConverterTests), and the remaining ~17 tree_style_reached_set_test.ts cases (scope cut for this port; the three ported exercise the End-token guard, the range-start guard, and the split-family carry — the three new mechanisms `styleTargets` adds). Verification: full suite 887 unit tests / 0 failures; live-server `Tree` filter 594 tests / 0 failures (9 pre-existing quarantined skips); swiftformat 0.55.6 and swiftlint 0.58.2 clean on every touched file. * Read a double without writing to the buffer it shares Port of yorkie-js-sdk#1422. iOS never had the shared-buffer write -- its reads are relative to the Data window and never mutable -- but it had the other half: a fixed-width payload shorter than its type trapped in UnsafeRawBufferPointer.load, taking the whole snapshot or change decode down. Converter.valueFrom / countValueFrom now read through loadZeroPadded, so missing bytes read as zero, as in JS. HLL.restore refuses a wrong-length payload or a register value no add could have written, returning false and leaving the registers unchanged instead of throwing; CRDTCounter.restoreHLL returns the result and only throws for a non-dedup counter. The two HLLTests that asserted the throw now assert the refusal; the JS short-payload and malformed-HLL tests are ported alongside. * Port three GC correctness fixes from the Go SDK Port of yorkie-js-sdk#1405. The server builds snapshots with Go semantics, so the SDK's garbage collection has to reach the same result. This ports three GC correctness fixes from Go, mirroring yorkie-js-sdk's port of them. Collection now holds a purge back until the RGA skip's successor is causally stable (GCParent/CRDTContainer gained a `purgeBarrierAt` requirement, defaulting to no barrier, overridden by RGATreeList, RGATreeSplit and CRDTTree, and delegated to by CRDTArray). RGA places a concurrent insert by walking forward past newer nodes, reading tombstones along the way, so a replica that has collected a tombstone could stop that walk somewhere a replica that has not collected it would not -- reordering a later concurrent insert. `CRDTRoot.collect` now reports what it purged and what it deferred, and `garbageCollect` repeats the pass while a round both purged and deferred, draining the backlog once the deferred successor becomes covered. The fix is partial, as in Go: an insert's own anchor is still unprotected. A value copied by a split or by a restore recreating a purged piece (`RGATreeSplit.bookCopiedAttrTombstones`, used by `splitNode` and by `restore`'s gap-recreate path) now books the attribute tombstones the copy duplicated, through `RGATreeSplitValue.getGCPairs()` (a new protocol requirement, default empty; `CRDTTextValue` already had the pairs). `CRDTTree.recreateFromSpan` books the same for a recreated non-text node's attributes. Without this the copy's tombstones were never registered, so they could never be collected and `docSize` drifted from a rebuild. `CRDTRoot.registerElement` now books the tombstones a Text/Tree/Array carries *inside* itself (removed tree nodes, removed text pieces, removed attributes, dead array positions) on every registration (`registerInternalGCPairs`), not only in `CRDTRoot.init`'s one-time snapshot-load scan. A remote Set/Add/ArraySet payload, or an undo re-setting a `deepcopy` of a removed container, both bring such an element in through `registerElement` and previously left its internal tombstones unbooked forever. Also: `CRDTTree.getGCPairs()` no longer books the tree's own root as a tombstone (purging is detachment from a parent, which the root has none of), matching Go. Divergences from the JS source: - `purgeBarrierAt` is a requirement on both `GCParent` and `CRDTContainer`, with a default `nil`-returning extension, rather than an optional method (`purgeBarrierAt?`) as in TypeScript: a Swift protocol extension method is not reachable through an existential unless it is also a requirement (bit #1404's `Operation.setActor` the same way), and `CRDTRoot.collect` calls it through `GCParent`/`CRDTContainer` existentials. - `RGATreeSplitValue.getGCPairs()` is likewise a requirement with a default empty implementation, for the same reason; JS's `RGATreeSplitValue.getGCPairs?()` is optional. - iOS's `CRDTTextValue.getGCPairs()` already existed (from an earlier port) and is unchanged; `bookCopiedAttrTombstones` now calls it instead of the narrower `getRemovedAttrs()`, which is removed (JS made the same removal). - `CRDTTree.innerChildren` is an ordinary stored `[CRDTTreeNode]`, not a getter that copies (JS's `allChildren`), so `purgeBarrierAt` reads it directly; no `_children` fast path was needed. - `CRDTRoot.garbageCollect`/`collect` keep the existing Swift error handling around `CRDTContainer.purge(element:)` (`do`/`catch`, logging and skip on failure -- pre-existing #1340 behaviour) rather than JS's unconditional call; the new barrier check is inserted before it, in the same position JS adds it. No proto/wire changes. * Cover the three GC correctness fixes Covers yorkie-js-sdk#1405 (Go TestRegisterElementBooksInternalTombstones, TestRegisterElementSkipsTombstonedTreeRoot, TestConcurrentDeleteAndInsertThenGCKeepsTextOrder, TestConcurrentDeleteAndInsertThenGCKeepsTreeOrder, and the live-server "successor barrier drains" scenarios yorkie-js-sdk ported from TestGarbageCollectionBarrierDrainsWithinOneRound). RootInternalGCPairTests (new, unit) ports root_internal_gc_pair_test.ts's four cases: a tombstone inside a raw-constructed registered Tree is booked and collectable; a crafted payload that marks the tree root removed never books the root itself; an undone container removal's orphaned tombstone is collected after gc and docSize matches a rebuild; and the same, decoded by a remote replica from a wire Set. GCSuccessorBarrierTests (new, unit) ports two of gc_rga_barrier_test.ts's three "GC successor barrier" cases -- Text and Tree -- each reproducing Go's WSX/WXS divergence: without the barrier a replica that collects a concurrently-deleted tombstone runs a later concurrent insert past a node the non-collecting replica stops at. GCIntegrationTests (existing, 8 `getGarbageLength` assertions updated from 0 to 1 and annotated): the successor barrier now retains one tombstone for a round in `test_concurrent_garbage_collection_test`, `..._with_pushonly`, `test_detach_gc`, and both `test_gc_attach_pushpull_detach_lifecycle_version_vector*` tests -- docSize matches yorkie-js-sdk exactly at every annotated point. GCIntegrationTests also gained `test_successor_barrier_drains_within_one_round` and `test_successor_barrier_drains_after_the_successor_author_detaches`, ported from the two new gc_test.ts integration cases, run against a local server. Not ported from gc_rga_barrier_test.ts: - "keeps insert order when a removed element was concurrently moved" and "keeps concurrent moves of one element converged" (Array/move): both exercise the same `RGATreeList.purgeBarrierAt` the Text/Tree cases pin, plus a second LWW position-register interaction; the live GCIntegrationTests successor-barrier tests exercise the Array path end-to-end instead. - The "GC successor barrier cost" describe block (retention bounded by lag not array size; a byte-limited array still accepting every move): quantitative ceilings on retention, not collection correctness. - gc_rga_fuzz_test.ts (RGA_FUZZ=1): an opt-in, assertion-free fuzz harness with its own PRNG; Go's equivalent is likewise opt-in and asserts nothing on CI. - Also not ported: `test_gc_targeting_nodes_made_by_deactivated_client` needed no `synchronous: true` port -- iOS's `Client.deactivate()` has no synchronous/asynchronous distinction (a single `async throws` call that awaits the server round trip), and the test was already green against the local server both before and after the production change. Verification: new unit tests fail (Red) with the three production commits' GC changes reverted -- confirmed WSX/WXS exactly as upstream's lessons file records for the Text/Tree barrier cases, and each RootInternalGCPairTests assertion failing as expected -- and pass (Green) restored (verified byte-identical via `git diff --stat` after restoring from a saved patch). Full unit suite 901 tests / 0 failures (1 pre-existing quarantined skip); GCIntegrationTests 24 tests / 0 failures against a local server; `GC|Array|Tree|History|Undo` filter 738 tests / 0 failures (10 pre-existing quarantined skips); swiftformat 0.55.6 and swiftlint 0.58.2 clean on every touched file. * Report where Tree undo and redo landed in their OpInfo Port of yorkie-js-sdk#1421. An identity-preserving Tree undo or redo published one placeholder tree-edit OpInfo with empty fromPath/toPath and no value, locally and on peers. An editor binding that maps OpInfo onto its own model could not tell what changed and had to re-read the whole tree after every undo or redo. `CRDTTree.restore`/`retombstone` now report one `TreeVisibleEdit` (a `TreeChange` plus the visible size it inserted) per node that comes back or leaves, measured at the moment it does (`visibleRangeOf`/`makeInsertionChange`, new), so the list applies one change after another like any other edit -- Text undo already worked this way. `TreeEditOperation`'s identity-preserving branch (factored out into `executeIdentityPreservingEdit` to stay under SwiftLint's function-length budget) now builds one `TreeEditOpInfo` per edit instead of a single best-effort placeholder, and records `executedRanges` from the same measurements for the undo stack (`getExecutedRanges()`, new): the stored `fromIdx`/`toIdx` describe the forward edit this op reverses and never move, so reconciling against them would shift pending entries by a range this op never touched. `Document.applyChangeInternal`'s undo/redo-stack reconciliation loop now iterates `getExecutedRanges()` instead of calling `normalizePos()`/`getContentSize()` once. A restore whose nodes all land under a removed ancestor now reports no OpInfo. `Document.executeUndoRedoInternal`'s `LocalChangeEvent` publish was still gated on `!opInfos.isEmpty`, although the surrounding `localChanges`/`changeID`/`onLocalChange` bookkeeping already correctly gated on operations having run -- the one place this port found iOS short of the fix's intent -- so it now gates on `!executedOperations.isEmpty` too, matching `applyChangeInternal`'s existing `change.hasOperations` gate on the remote side. Public API: no new fields. `TreeEditOpInfo` already carried `from`/`to`/`value`/`fromPath`/`toPath`; an identity-preserving Tree undo/redo now populates them instead of always publishing one entry with `from`/`to` taken from the reversed op's stored (never-moving) indices and empty `value`/`fromPath`/`toPath`. Observable effect: a subscriber now receives zero-to-many populated `TreeEditOpInfo` entries per Tree undo/redo instead of exactly one empty one. No proto/wire change: `TreeChange`/`TreeVisibleEdit` are in-process types, and what crosses the wire is still the operation's existing `restoreSpans`/`retombstoneSpans`. Also: `Tests/Unit/Document/CRDT/TreeRestoreTests.swift`'s `tree.restore`/`tree.retombstone` call sites updated for the new return arity (5-tuple and 3-tuple); no behavioural change to those tests. iOS already had no counterpart to port for: - The event-gating fix in `Document.update`'s forward-edit path and in `applyChangeInternal`'s remote path: both already gate on operations that ran (`change.hasOperations` / `executedOperations`), not on `opInfos`. - The internal local-change queue signal (`Document.subscribeLocalChangesInternal` in JS): iOS's offline persistence (`Client.installOfflinePersistence`) already drives off `Document.onLocalChange`, a plain closure `update()` and `executeUndoRedoInternal` both call unconditionally whenever `context.hasChange` is true (operations or a presence change) -- before any public event publishes, and regardless of whether `opInfos` is empty. It was never leaning on the public event stream the way JS's `doc.subscribe('all', ...)` was, so there was nothing to replace. - The webhook test's loopback-host fix (`webhookAddr`/ `isYorkieContainerRunning`): a CI-environment concern specific to the JS test runner's Docker detection, with no iOS counterpart. * Cover reporting where Tree undo and redo landed Covers yorkie-js-sdk#1421 (test/unit/document/tree_undo_opinfo_test.ts). TreeUndoOpInfoTests (new, unit) ports both of tree_undo_opinfo_test.ts's cases: an undo whose revived nodes land under a peer's concurrently-removed ancestor publishes its LocalChangeEvent exactly once with an empty operations list (rather than publishing nothing, or publishing a non-empty placeholder), and still reaches the peer -- confirmed by the peer's version vector advancing for the undoing actor; and a redo of the same scenario, after both replicas have collected the tombstone, leaves a recreated node tombstoned under its restored-but-still-removed-at-that-moment parent (`recreateFromSpan`), identically on both peers. Not ported: test/integration/history_tree_opinfo_test.ts (the 5 parametrized "reports where undo/redo landed" cases over different edit shapes, "reports shifted positions when a peer edited before the undone text", and "reports a restored subtree parent first, then its children"). These exercise the same per-node opInfo mechanism this change adds, driven through a live two-client integration harness and a "follow" mirror document that replays published OpInfos with editByPath/editBulkByPath. TreeUndoOpInfoTests already exercises the identical "nothing to report" and GC-interaction scenarios across two replicas via wire-format ChangePack delivery (the same no-live-server pattern this session's GCSuccessorBarrierTests established), which is the behavior this port is actually responsible for; the additional parametrized cases would mostly re-confirm the unchanged normal (non-restore) opInfo path already covered elsewhere. Also not ported: webhook_address_test.ts (CI loopback-host detection, no iOS counterpart) and offline_local_queue_test.ts (iOS's `Document.onLocalChange` hook already satisfies the invariant that suite pins for JS's new `subscribeLocalChangesInternal`; see the source commit's divergence notes). Verification: both new tests fail (Red) with the production commit's CRDTTree/TreeEditOperation/Document changes reverted -- "reports no position" fails exactly on the empty-operations assertion, since the pre-fix code publishes a non-empty placeholder opInfo instead of nothing -- confirmed byte-identical restore via `git diff --stat` after a saved patch -- and pass (Green) restored. Full unit suite 901 tests / 0 failures (1 pre-existing quarantined skip); scoped integration filter (GCIntegrationTests, TreeHistory*, TreeConcurrency, TreeIntegration, TreeMerge, TreeSplit, TreeStyle, UndoRedo) 370 tests / 0 failures (9 pre-existing quarantined skips) against a local server; swiftformat 0.55.6 and swiftlint 0.58.2 clean on every touched file. * Let a document choose its undo/redo depth (#1424) Port of yorkie-js-sdk#1424. DocumentOptions.maxUndoDepth sets how many entries History keeps on each of the undo and redo stacks before dropping the oldest, mirroring JS's doc.history depth option. History(maxDepth:) now takes the depth instead of hard-coding maxUndoRedoStackDepth (default unchanged: 50), and Document.init(key:opts:) resolves it once and hands it to the History instance it owns. History gained @unchecked Sendable: Document.init(key:opts:) is nonisolated (constructible off the main actor, like the rest of Document's public API), and assigning a depth-configured History instance to the @MainActor-isolated internalHistory property from that init requires the stored type to cross isolation safely. Every access to it is already serialized through the @MainActor Document that owns it exclusively, so this is a visibility annotation, not a new thread-safety mechanism. Divergence: JS throws ErrInvalidArgument from the Document constructor for a maxUndoDepth below 1 or non-integer. Swift's Int cannot represent a non-integer, and making Document.init(key:opts:) throws would be a breaking change for every existing call site, so this stays non-throwing by design and silently falls back to the default depth for a value below 1 instead of rejecting it. Public API added: DocumentOptions.maxUndoDepth (Int, default 50). * Cover document-chosen undo/redo depth Ports packages/sdk/test/unit/document/undo_depth_test.ts (yorkie-js-sdk#1424). DocumentMaxUndoDepthTests mirrors the 'Document maxUndoDepth' describe block end to end through Document.update/undo/redo. HistoryDepthTests mirrors 'History depth', reaching History directly since it is internal rather than through a cast as the JS test does. Not ported: JS's rejects-a-maxUndoDepth-of assertions for 1.5, NaN and +Infinity. Int cannot represent any of them, so those three cases do not exist on this platform. The 0 and -1 cases are ported but adapted: since Document.init(key:opts:) stays non-throwing (see the source commit), they assert the default-depth fallback instead of a throw. * Burn the lamport when a change's root pass throws (#1397) Port of yorkie-js-sdk#1397. Change.execute does not roll back, so an operation that throws on the root pass of Document.update or of an undo/redo leaves the prefix that already ran in the root. The change is never queued and changeID was left as it was, so the next change built its context from the same ID, reissued the TimeTickets the prefix had burned and took over the prefix's slots in the root's createdAt registry: two live elements under one id. On that path, update()'s commit step and executeUndoRedoInternal's root pass now advance the lamport (and this actor's version vector entry) so no later ticket can collide, keeping clientSeq, which names queued changes to the server and must stay gapless. update()'s root-pass change.execute was swallowing this throw outright (`try?`, no catch), silently treating a failed root pass as if nothing had run and letting update() return without error -- a pre-existing iOS divergence from JS, which always surfaces it. Fixing it is what this port's catch block needs to burn the lamport in the first place, so update()'s tail (previously inline) is factored into a private commitLocalChange(context:actorID:), called from update() and from the new updateWithOperationForTest(_:presence:) test hook; no behavior changes beyond the divergence fix itself. Divergence: JS's equivalent catch around executeUndoRedo's root pass is new in this commit; iOS's executeUndoRedoInternal already had its outer executeUndoRedo drop the clone on any throw (ported earlier, 0.7.21/#1394), so only the lamport-burning catch needed adding there. * Cover the clone/lamport fixes for a failed root pass Ports the four tests packages/sdk/test/unit/document/clone_reset_test.ts added alongside yorkie-js-sdk#1397. Added to DocumentCloneResetTests.swift, next to the 0.7.21/#1394 clone- reset tests it builds on. Document.update mutates the clone directly through the JSONObject proxy rather than through Operation.execute, so the single change.execute(root: self.root) call these tests drive through the new updateWithOperationForTest(_:presence:) test hook IS the root pass -- JS reaches the same call by spying on SetOperation.prototype.execute, which Swift cannot patch. FaultInjectingSetOperation extends the existing FaultInjectingOperation fake with a real write (CRDTObject.set + CRDTRoot.registerElement) on its non-failing calls, so a two-operation change ("a" lands, "b" throws) leaves genuinely observable state -- needed to port "records nothing" and "does not reissue the tickets", where JS's real SetOperations are the ones doing the writing. Not ported: no JS test was skipped; all four were ported. * Re-point operations at the elements a redo split re-creates Port of yorkie-js-sdk#1426. A change that splits a Tree element and styles the split-born piece -- what an editor produces when bolding part of a word -- could be undone and redone locally, but after garbage collection peers rejected the redo with "cannot find node of CRDTTreePos" and kept failing on it. The redo of a split mints the split-born element under a new id, while the style recorded after it still named the element the undo had merged away, which GC then purged. A split that re-creates merged-away elements now reports their ids (CRDTTree.mergeSourceOf), and the operations still pending in the entry and both history stacks are re-pointed from each old id to the newly minted one before the change is encoded. The re-pointing follows the tickets the split actually consumed, covers splits made by a local edit, an undo/redo and a peer's change, and is scoped to the tree the split ran on -- the tree counterpart of reconcileCreatedAt. iOS-specific notes: - CRDTTree.edit(...)'s return tuple grows by two elements (toBeMergedNodes, splitRecreatedIDs); the three call sites (TreeEditOperation.execute, CRDTTree.editT, JSONTree.editInternal) and two test call sites were updated for the new arity. The insert phase (step 05) was extracted into CRDTTree.applyInsertPhase to keep edit(...) under this project's function-length limit. - The undo/redo "careful part" (per-ticket onSplitTicketConsumed handler, pending-list drain against the root pass's actually-executed operations) is ported faithfully into a new file-scoped UndoRedoTreeNodeIDRepointer type in Document.swift, kept file-scoped rather than nested to stay within this project's nesting-depth and type-body-length limits. - JSONTree.editInternal's own direct tree.edit() call (the clone-pass local-edit path) discards splitRecreatedIDs, matching yorkie-js-sdk's own json/tree.ts, which does the same at its analogous call site: the authoritative signal comes from the root-pass execution inside TreeEditOperation.execute. * Cover re-pointing the elements a redo split re-creates Ports packages/sdk/test/unit/document/tree_redo_split_style_test.ts (yorkie-js-sdk b173c093, #1426) in full: two-client convergence over directly exchanged change packs (no server), covering - a split and a style landing in the SAME undo/redo entry, across five edit shapes (mid-word split, end-of-word split, two-level split styling the new paragraph, two-level split styling the new inline, and a split followed by a plain insert into the new element), each run with and without an intervening garbage collection, - a split and the style naming its element landing in DIFFERENT history entries, so only History.reconcileTreeNodeID's stack sweep -- not the same-entry repointRest path -- can fix it up, with and without GC, - a plain (non undo/redo) local split re-creating a block a peer's merge removed, from both the splitting side and the receiving side, and - History.reconcileTreeNodeID itself, scoped to one tree. With History.reconcileTreeNodeID stubbed to a no-op (verifying the fix matters, patch saved and reverted -- git diff --stat identical before/after), the 5 same-entry cases still pass via the inline repointRest path, but the after-GC variant of the cross-entry case and both plain cross-replica split cases fail -- exactly the shape of the original bug, which needs GC to have actually purged the stale element before a peer rejects the redo. * Sync the protos with Go and harden crafted tree payloads Port of yorkie-js-sdk#1406. The .proto files are re-synced with Go's main (73139d01): the new fields are ChangePack.capabilities, restore_mode on Operation.Set/Add/ArraySet, revived_at on every JSONElement subtype, and Project/ UpdatableProjectFields.channel_session_ttl, plus several comment-only changes (RestoreMode's revive-vs-copy rationale, WatchRequest.resources, TreeNode.merged_at). Regenerated with the pinned buf plugins (swift v1.37.0, connect v1.0.3); `git diff` on Generated/ touches only `_protobuf_nameMap` tables and comments. All of the new fields are dormant: upstream's own converter does not read or write any of them yet (restore_mode is read only on Edit/TreeEdit, which this SDK already had), so no Swift logic is wired to them either. Every merge pointer now resolves to the exact element it names (yorkie#2033), not a floor match or a text node, since mergedFrom/ mergedInto can be client-supplied on an element payload (Set/Add/ ArraySet carries the lineage a reverse-of-Remove legitimately keeps). `CRDTTree.findMergeNode` -- added by the #1404 port but so far only used by the style-targeting machinery -- now backs rebuildMergeState, resolveMergeTarget, the merge-destination insert redirect in findNodesAndSplitText, applyMergeMoves' mergedInto stamp, and propagateDeletesToMergedChildren. rebuildMergeState and applyMergeMoves also gained the "source must already be a tombstone" guard Go requires, and rebuildMergeState a "destination must be an element" guard: without either, a crafted mergedFrom could plant a forwarding pointer on a live, unrelated element and arm the merge-delete cascade to tombstone that element's own children later. CRDTTreeNode.unremove() now also clears mergedInto (derivable only while removed), and CRDTTree.restore gained dissolveMerge to erase a revived source's forwarding pointer and its moved children's mergedFrom, so an undo and a snapshot load agree. TreeEdit content is stripped of engine-only state on decode, matching Go: CRDTTreeNode.dropMergeStamps is renamed dropEngineOnlyLinks and now also clears insPrevID/insNextID (content can never legitimately be a split product either), and the new clearTombstones revives every node a crafted payload tombstoned, restoring ancestor sizes in post-order. Attribute (RHT) tombstones on content are deliberately left alone -- genuine for the undo copy-reinsert path, and stripping them would diverge from every other decoder of the same bytes -- but are now booked into gc via CRDTTreeNode.getGCPairs() from the insert phase, the same routing the snapshot and Set/Add paths already use; held aside from the pairs the spansComplete count reads, same as upstream. The insert phase's "born dead under a removed parent" GCPair now carries gcOnlySize, matching upstream: without it, live was debited for bytes that were never added to it. Converter.fromTreeNodesWhenEdit's "entry with no node" rejection and fromTreeNodes' parent-depth/text guard predate this port (#1404) and needed no change. Divergences from the JS source: - iOS's Converter.fromTreeNodes wraps decoded TreeEdit content in a full CRDTTree (to reuse its traversal/size machinery), so decoding such content already runs rebuildMergeState, unlike JS's standalone node-builder; the sanitizer still has to run after it for the same reason it does in JS. - dissolveMerge and resolveMergeTargetForTest live next to resolveMergeTarget rather than at the end of the class, to keep related merge-lineage helpers together; no behavioral difference. No change to what this SDK sends on the wire: the new proto fields are additive and unpopulated, and the hardening only affects how a payload this client receives is decoded, not what it produces. * Cover the proto sync and crafted tree payload hardening Ports both new JS test files from yorkie-js-sdk#1406. TreeEditContentSanitizeConverterTests mirrors tree_edit_content_sanitize_test.ts: a TreeEdit's content round-tripped through the converter has a crafted tombstone revived (with ancestor sizes restored), a crafted merge lineage (mergedFrom/mergedAt/ mergedInto) dropped, split links dropped, a genuine attribute tombstone kept AND booked into a GC pair with gcOnlySize, and an empty content group rejected. The merge-lineage case adds an iOS-specific guard assertion: unlike JS's converter, this SDK's decode path wraps content in a full CRDTTree and so really does run rebuildMergeState over the fixture before the sanitizer strips what it derived, which the guard demonstrates directly (JS proves the same decoder behavior separately, against its own small tree, since its converter never constructs one). CRDTTreeMergeLineageTests mirrors tree_merge_lineage_test.ts: the same <r><p></p><p>cd</p><p></p></r> fixture pins that rebuildMergeState, resolveMergeTarget, the merge-destination insert redirect, an ordinary merge, and the merge-delete cascade all resolve mergedFrom/mergedInto by exact element id -- a forged offset, a live source, and a text node as either source or destination are each rejected by a different one of the new guards, with the genuine-lineage cases as positive controls. Mutation-tested by reverting the production commit's logic (saved and restored via a patch; `git diff --stat` confirmed identical before and after, and no new-test files were touched) and rerunning: 9/11 CRDTTreeMergeLineageTests cases and 2/6 TreeEditContentSanitizeConverterTests cases (the tombstone-revival case and the attribute-tombstone GC-booking case) went red, with the two genuine-lineage positive controls staying green by design. The split-links and empty-content-group cases also stayed green under the revert, since iOS already covered that part of the hardening before this port (#1404); so did the merge-lineage-dropping case, since the pre-existing dropMergeStamps already cleared those fields unconditionally -- it is kept as a regression lock, as upstream's own test is. Not ported: upstream's separate fixture-only assertion proving its decoder "really does derive a mergedInto" is folded into the one guard assertion above rather than kept as a second, parallel check, since iOS's real decode path already demonstrates it. * Push local edits during IME composition in the PM binding Port of yorkie-js-sdk#1393. Only the SDK client half applies to iOS: the ProseMirror binding fix (pausing with RealtimePushOnly instead of RealtimeSyncOff, applying deferred remote changes, URL-attribute sanitizing) has no iOS counterpart -- there is no ProseMirror binding in this SDK. The SDK-level fix does apply. `Client.syncInternal` treats the reply to a push-only request as a push ack only: client-seq acknowledgement and pack metadata, never remote state and never its version vector. Previously `dropsRemoteState` also required the response to carry changes or a snapshot, so a push-only reply with neither (the common case) fell through to `applyChangePack`, which runs GC with the reply's version vector even though the document is paused and has not pulled the remote changes that vector's tombstones may anchor on -- those changes then fail to apply on the next full pull ("cannot find node") and the server redelivers the same pack forever. `dropsRemoteState` now keys only on the mode the request was sent in (captured before the request, since the attachment's mode can change while the request is in flight) and the attachment's current mode, matching upstream exactly. iOS's existing `Document.acknowledgePushedChanges` (ported with yorkie-js-sdk#1401 in a prior sync) already covers the push-only snapshot-drop half of this; this change only widens when that path is taken. No other divergence from upstream's SDK-side fix. * Cover the push-only GC ack fix Ports "must not collect a tombstone a deferred remote change anchors on" (yorkie-js-sdk#1393, pushonly_gc_test.ts). Two clients converge on a tree; c2 enters push-only (simulating IME composition), replaces a character (tombstoning it) while c1 inserts right after the live character unaware of the replacement, then c2 keeps composing through a push whose reply carries a minimum version vector under which the tombstone is collectable. With Client.swift's fix reverted, this fails exactly as described upstream: ErrChangeApplyFailed / "cannot find node" when c2 resumes and pulls the deferred insert. * Read Text.normalizePos from the index tree instead of the chain Port of yorkie-js-sdk#1442. normalizePos summed node.length over the whole physical prev chain, so each call was O(n) in the number of split nodes. EditOperation.execute calls it on every local edit to build the reverse operation, so typing a document was quadratic in its length. The sum is already in treeByIndex: every chain node is in it, in chain order, weighted by its live length (a tombstone stays in at weight zero; only purge, which also unlinks the chain, takes a node out). So the offset is treeByIndex.indexOf(node) plus the in-node relative offset, anchored on the head -- the same result the chain walk gave for every input, since indexOf is defined as exactly that sum. This ports the Go fix (yorkie#2107) and the JS change one-to-one; no other RGATreeSplit path changes. * Cover the normalizePos index-tree read Ports a subset of text_normalize_pos_test.ts (yorkie-js-sdk#1442): - test_matches_the_chain_walk_across_edit_style_undo_redo_and_gc and test_resolves_positions_whose_pieces_gc_purged_as_the_chain_walk_did check the new normalizePos against a literal reference implementation of the chain walk it replaced, across randomized edit/style/undo/redo/GC sequences and for positions whose pieces GC purged -- the correctness properties the fix must preserve exactly. - test_keeps_typing_cost_from_growing_quadratically_with_document_length replaces JS's two hop-counting tests (which monkey-patch RGATreeSplitNode.prototype.getPrev to count chain hops deterministically, without a clock). Swift has no prototype to patch, and adding a hop counter to RGATreeSplitNode purely for a test would be production code shaped by a test, so this measures wall-clock typing time instead: typing 8,000 characters took ~11x as long as typing 2,000 (four times the text) on the pre-fix chain walk in this run, against ~4.5x post-fix; the threshold sits between the two with margin on both sides. Not ported: JS's "should match the chain walk on replicas applying remote edits" multi-replica fuzz. It exercises the same normalizePos code path the local fuzz test above already drives through GC and undo/redo; adding a second, cross-replica fuzz of the same invariant was left out of this port's scope. Verified the correctness tests still pass and the timing test fails (11.0x, threshold 7.5x) with RGATreeSplit.swift's fix reverted -- the chain-walk equivalence holds whether or not the fix is applied (it is a pure performance change), so only the timing test is sensitive to it, as expected. * Reject indexes that split a UTF-16 surrogate pair Port of yorkie-js-sdk#1447. Text and Tree indexes count UTF-16 code units -- CRDTTextValue.content and CRDTTreeNode.value are both NSString, matching JS's string.length, specifically so this SDK's offsets stay wire-compatible with JS's -- so a local index between the two halves of a non-BMP character split the node mid-pair. Go turns each lone half into U+FFFD while JS (and, via NSString, this SDK) keeps the raw code unit, so the same operation left different text on different SDKs. This is the JS half of yorkie-team/yorkie#2085. Added splitsSurrogatePair/isUTF16Boundary/ensureUTF16Boundary/ ensureNoLoneSurrogate to Json/Strings.swift, the iOS home for the string helpers JS keeps in document/json/strings.ts. CRDTText gained createRange(fromIdx:toIdx:content:), the checked counterpart to the pre-existing indexRangeToPosRange, used by JSONText's edit/setStyle. CRDTTree.findPos now rejects a text position that splits a pair; findPosUnchecked is the old unchecked behavior, used by indexRangeToPosRange/indexRangeToPosStructRange (selection conversions), the second (derived) position in pathToPosRange, and -- matching upstream's companion change to tree_edit_operation.ts in the same commit -- every tree.findPos call in TreeEditOperation.swift (undo-op reconciliation, redo-split, reverse-operation building): those are indexes the document computed for itself, not a caller's, and refusing them would only break undo/redo and remote apply. JSONTree's validateTextNode (already called from every local edit path via editInternal/validateTreeNodes) gained the lone-surrogate check for inserted text content. Newly-rejected public inputs, all as YorkieError(code: .errInvalidArgument): - JSONTree.edit, editBulk, editByPath, editBulkByPath, style, removeStyle, styleByPath (both overloads), removeStyleByPath (both overloads), splitByPath -- all already declared `throws` and propagate the error unchanged, same as any other invalid-argument rejection these methods already had. - JSONText.edit and setStyle keep their existing failure style: they catch internally, log, and return nil / false respectively, rather than throwing out of Document.update. This means a rejected JSONText edit does NOT roll back an earlier, valid edit in the same Document.update closure the way JSONTree's throwing methods do -- Document.update only drops its clone when the updater itself throws. This is a pre-existing property of JSONText's wrapper, unchanged by this port, and is pinned by test_Text_edit_does_not_roll_back_an_earlier_edit_in_the_same_update. Indexes the document computes itself (undo/redo reconciliation, reverse-operation builders run on remote apply) and selection conversions (indexRangeToPosRange/indexRangeToPosStructRange/ createRangeForTest) stay unchecked, so remote changes and presence never throw -- matching upstream. Not ported: the XML/JSON attribute-escaping hardening in the same upstream commit (escapeXMLAttr in tree.ts, JSON.stringify instead of String(value) in CRDTTextValue.toJSON). That is a separate, orthogonal concern (peer-chosen attribute values breaking out of the JSON/XML they are interpolated into) bundled into the same upstream commit as the surrogate-pair fix; it is not part of the "reject indexes" contract this change ports and is left for its own port. * Cover the UTF-16 surrogate-pair index rejection Ports a representative subset of surrogate_index_test.ts (yorkie-js-sdk#1447): - isUTF16Boundary matches an independent encoded definition, and rejects only the offset inside a pair. - Text.edit/Tree.edit refuse lone-surrogate content (4 cases each), and accept whole characters on both sides of a pair. - Text.edit, Text.setStyle, and all nine Tree entry points (edit, editBulk, style, removeStyle, editByPath, editBulkByPath, styleByPath, removeStyleByPath, splitByPath) reject a mid-pair index and leave the document unchanged. - Valid Text/Tree boundaries around the pair stay editable. - A rejected Tree edit discards an earlier, valid edit in the same Document.update closure (Document.update drops its clone when the updater throws); the Text counterpart pins the opposite, documented outcome instead (see the source commit's message). - Undo/redo still resolves correctly around an intact pair, compared via toXML()/.toString rather than toSortedJSON(): inserting mid-node splits the underlying RGA node, and undo does not re-merge it, so the internal JSON shape differs from the pre-edit document even though the visible content is identical. A Swift string literal cannot spell a lone surrogate ("\u{D83D}" does not compile -- not a valid Unicode scalar), so lone-surrogate test values are built from raw UTF-16 code units via NSString(characters:length:), verified empirically to round-trip losslessly through `as String` and back. Not ported: JS's "Mid-surrogate-pair indexes the document computed" cases, which craft raw wire ChangePacks to simulate an older client sending a mid-pair remote operation, and its fine-grained "resolves indexes after a split" / "local seam" cases. The former need low-level protobuf scaffolding this suite does not otherwise use; both exercise the same findPosUnchecked routing already covered by this suite's rollback and undo/redo tests, and that routing was additionally verified by direct code audit against every tree.findPos call site in TreeEditOperation.swift. Verified: all 13 tests fail (9 of them; the other 4 are unaffected, being either pure-helper or valid-input cases) with the CRDTText.swift/ CRDTTree.swift/JSONText.swift/JSONTree.swift wiring reverted (Strings.swift's new helpers kept, so the file still compiles) -- e.g. mid-pair edits silently succeed and produce corrupted (replacement-character) text instead of being refused. * Escape the attribute keys and values toXML and toJSON interpolate The rest of yorkie-js-sdk#1447, which the surrogate-pair port left out. Tree toXML now escapes both the key and the value of every attribute and always quotes the value -- a non-string renders as its stored JSON form, so `bold=true` becomes `bold="true"`, as yorkie-js-sdk renders it -- so a peer-written value holding `"` or `<` cannot forge an attribute or an element. CRDTTextValue.toJSON re-encodes a non-string value and JSON-encodes the key instead of interpolating them. iOS's toJSON already emitted parsed non-string values as valid JSON, so only the key encoding changes there; String.escaped() turned `'` into `\'`, which is not a JSON escape. Thirteen test expectations move to the quoted XML form. * Cover attribute escaping in toXML and toJSON * Run the normalizePos timing check only when asked for A wall-clock ratio is not stable on shared CI runners. The check runs with YORKIE_PERF_TESTS=1; the correctness fuzz tests still run every time. * Cover the remaining tree style reached-set cases Ports the rest of yorkie-js-sdk's tree_style_reached_set_test.ts as an in-process batch-replay unit suite (grab/feed/seed harness), matching the file's own single-replica shape: the two removeStyle mirrors of the already-ported split/merge cases, the 13 complex-suite scenarios, the level-2-split-before-the-range pair, the split-and-merge-in-one- change pair, and the styleByPath backwards-range rejection. The exhaustive scan tests at the end of the JS file are not ported; see the new test file's header comment for why. * Cover the style tombstone cases the GC barrier port added Ports the two cases yorkie-js-sdk's style_tombstone_test.ts gained alongside the Go GC-barrier fixes: a purged tree node and a purged text piece each carry their attribute RHT's tombstones through recreateFromSpan/substring, and those copies have to be booked as GC pairs or they sit uncounted and unpurgeable. Both pass unmodified, confirming the booking already lands on this branch. * Cover the Tree undo/redo op-info edit shapes Ports the seven remaining cases of yorkie-js-sdk's history_tree_opinfo_test.ts as in-process unit tests (bidirectional crossSync instead of a live server): the five parametrized edit/undo/ redo shapes, the shifted-position case, and the parent-then-children restore case. A TreeHistoryFollower mirrors a replica's published tree-edit OpInfos into a second document the way an editor binding would, matching the JS harness's own approach. Four of the seven (delete element, delete two elements, insert element's redo, and the parent-then-children case) expose a confirmed iOS divergence: restoring a multi-node subtree in one change reports the parent's TreeEditOpInfo with its child already embedded, even though the child is also reported in its own separate OpInfo right after -- so replaying every OpInfo literally duplicates the child's content. The underlying CRDT document still converges correctly on both replicas; only the published OpInfo is wrong. Root cause: CRDTTree.makeInsertionChange (Sources/Document/Crdt/CRDTTree.swift:3357-3372) stores a live CRDTTreeNode reference instead of eagerly snapshotting it, and TreeEditOperation.executeIdentityPreservingEdit's opInfo construction (Sources/Document/Operation/TreeEditOperation.swift:579-594) converts every queued node only after the whole restore/retombstone loop has run, by which point a later span's unremove() has already made a sibling's child look live to the recursive node-to-JSONTreeNode conversion. JS avoids this by snapshotting eagerly inside the per-span loop (tree.ts's makeInsertionChange calls toTreeNode(node) immediately). These four cases are left in place with XCTExpectFailure so a future fix flips them back to passing. * Snapshot a restored Tree node for its OpInfo when the edit is made Fixes a defect in this branch's port of yorkie-js-sdk#1421, found by the history_tree_opinfo cases. A Tree undo/redo that restores a subtree by identity converted every reported node to a JSONTreeNode only after the whole retombstone/restore loop, so a parent's OpInfo embedded the children a later span had revived -- which are also reported on their own -- and a consumer replaying every OpInfo inserted them twice ("cd" -> "cdcd"). makeInsertionChange now captures the node as the edit is made, as JS's restore does with toTreeNode. * Expect restored Tree subtrees to report each node once Drops the XCTExpectFailure wrappers the history op-info cases carried for the deferred snapshot. * Bump CI yorkie server to v0.7.24 * Bump yorkieVersion to 0.7.24 * Add the v0.7.24 CHANGELOG entry * Address the pre-PR review of the 0.7.24 port - Document.update publishes its LocalChangeEvent when an operation ran, not when one produced an OperationInfo, as yorkie-js-sdk#1421 does and as the undo/redo path here already did; onLocalChange now fires after the change id advances, matching JS and the undo/redo path. - A boolean primitive reads any nonzero byte as true and an empty payload as false, as JS does; `data[0]` trapped on an empty payload. - A dedup counter decodes its wire value before restoring the HLL, and a rejected HLL payload is logged and dropped, so it keeps that value as in JS instead of reading zero. - A maxUndoDepth below 1 logs a warning when it falls back to the default. * Cover the pre-PR review fixes Also refines the v0.7.24 CHANGELOG: the GC successor barrier, LocalChange and errChangeApplyFailed notes, and the boolean/HLL decode line. * Link the v0.7.24 CHANGELOG entries to the PR * Wait for the deactivated client's detach in the GC successor test test_gc_targeting_nodes_made_by_deactivated_client failed on CI (expected 0 garbage, got 1). The server answers a deactivate first and detaches the client in the background; until then the minimum version vector does not cover the tombstone's successor, so the successor barrier from yorkie-js-sdk#1405 holds the tombstone back. JS deactivates with `synchronous: true`, which fails on the memory backend CI runs, so the test now syncs until the collection happens. * Keep a dedup counter's value across a deepcopy Raised by CodeRabbit. A dedup counter decoded with a rejected HLL payload keeps its wire value over empty registers, but deepcopy re-derived the value from those registers, so the Document clone (what getRoot reads) reported zero. The copy now carries the source value after restoring the HLL. Also drops a stale "iOS divergence" note from the history op-info tests, describing a defect fixed earlier in this branch. * Skip the GC successor-barrier check when the server never detaches The wait added in 11030f16f1 was not enough: on CI the garbage stayed at 1 for the whole wait. CI runs the yorkie server on the memory backend, where the detach a deactivate starts fails ("change not found", as yorkie-js-sdk notes in the same test), so the deactivated client's version vector row is never dropped and the successor barrier keeps the tombstone. JS runs this test against a Mongo-backed server. The test still asserts the pre-sync state, and skips the final collection check when the server never detaches. * Skip the GC successor-barrier check only on a memory backend YORKIE_SERVER_BACKEND (default: memory, what `yorkie server` and CI run) gates the skip, so a run against another backend asserts the tombstone is collected instead of skipping.

Commit:a77ff58
Author:longhoang-lewy
Committer:GitHub

Apply updates from yorkie-js-sdk 0.7.20 (#274) * Reject duplicate attach of the same key on one client Rapidly re-attaching a document key whose previous attach is still in flight sent a second AttachDocument for the same key. The server reports the already-attached key as a misleading ErrClientNotFound, which handleConnectError escalates into deactivating the whole client. Guards attach locally with a document-scoped ErrAlreadyAttached before the request is sent. attachmentMap covers the resolved case; a new attachingDocs set covers the concurrent in-flight case, since attachmentMap is only populated once the attach round trip resolves. Swift uses defer rather than upstream's finally to clear the key, so it is released on every exit including a thrown error. * Add epoch and stable actor_id fields to the protocol Regenerates the Swift protobuf for the three fields yorkie-js-sdk#1338 adds, which offline persistence and stable-actor watch depend on: - ChangePack.epoch = 8, so a resumed session can present the persisted epoch alongside its checkpoint and have the server re-accept re-pushed changes. - ActivateClientResponse.actor_id = 2, the stable actor the server derives from the project and client key. - WatchRequest.actor_id = 3, declaring that actor on the watch stream so peer ids in watched/unwatched events match the presence CRDT keying. All three are new field numbers, so the change is additive and wire-compatible: a client that omits them behaves exactly as before and the server falls back to client_id as the actor. Generated with the pinned buf plugins (apple/swift v1.37.0, connectrpc/swift v1.0.3). The only removed lines are the three _protobuf_nameMap entries replaced by versions carrying the new fields; yorkie.connect.swift is unchanged because no service changed. * Add DocStore and SessionLock, the storage seams for offline resume Ports the two pluggable interfaces yorkie-js-sdk#1338 introduces, ahead of the serialize and resume logic that uses them. DocStore is a three-method async protocol keyed by document key, with an in-memory default. Data is a value type, so the defensive copies upstream makes around its Uint8Array buffers are unnecessary here. SessionLock keeps two sessions from resuming the same persisted document and re-pushing the same un-acknowledged changes. Upstream's default is built on the browser Web Locks API to keep tabs apart and is documented there as a no-op outside a browser. iOS has no tabs and an app is one process, so NoopSessionLock is the default and matches that non-browser behaviour rather than emulating Web Locks. The hazard does remain reachable on Apple platforms when a persistent store is shared between an app and its extensions through an App Group, so the protocol is public and the documentation points a caller needing that at NSFileCoordinator or a lock file. * Bump version to 0.7.20 and pin CI yorkie server to v0.7.20 Unlike the last three releases this one is not client-side only: offline resume needs a server that accepts a re-pushed change pack against a persisted checkpoint and epoch, and that supports stable-actor watch (yorkie#1969, yorkie#1970). The CHANGELOG entry carries that minimum explicitly, matching how v0.7.14 and v0.7.16 flagged theirs. Homebrew has two "update 0.7.20 bottle." commits. Pinned to the later one, which adds an arm64_golden_gate bottle and carries different sha256s for the platforms the earlier commit already had — the bottles were rebuilt, so the earlier hashes may no longer match what is hosted. * Serialize and restore a Document for offline resume The Document half of yorkie-js-sdk#1338: a document plus its un-acknowledged local changes can be written to bytes and read back in a later session, so those changes can be re-pushed against the persisted checkpoint and epoch. toBytes packs six blobs behind 4-byte little-endian length prefixes, in upstream's order: snapshot, checkpoint, changeID, pending changes, epoch, docID. The framing matches upstream exactly; the payloads are where this diverges. Three deliberate divergences: - Pending changes are serialized as protobuf rather than upstream's JSON change-structs. TypeScript gets toStruct nearly free from structural typing; Swift would need hand-rolled Codable for every operation type, and Converter already round-trips a Change. The blob is local-only and never crosses SDKs, so only round-trip fidelity within iOS is required. Revisit if a persisted blob ever moves between SDKs. - getPendingChangeStructs returns [Change]. Upstream's struct layer exists only to feed that JSON encoding, so there is nothing to invent here. - toBytes and the restore entry points throw, where upstream's toBytes does not: snapshotToBytes calls the already-throwing Converter.toElement, and swallowing that would mean a force-unwrap. This follows Converter.objectToBytes and treeToBytes, which throw for the same reason. Also wires epoch through applyChangePack and createChangePack, without which the field would be dead state, and adds Converter.snapshotToBytes as the encode counterpart to the existing bytesToSnapshot. The unpacker bounds-checks both the length prefix and the slice it implies before reading, so a truncated or hostile envelope throws rather than trapping. Four of the ten tests cover malformed input, and the framing is pinned: flipping the length decode to big-endian fails them. * Wire offline persistence and the stable actor into Client The Client half of yorkie-js-sdk#1338. Stable actor: activate now learns actor_id from the server and stamps it into document changes instead of the session id, and declares it on both watch streams so peer ids in watched/unwatched events match the presence CRDT keying. An older server sends no actor_id, in which case the session id remains the actor and nothing changes. Exposed as getActorID(). Offline persistence, active only when a store is configured. Attach takes a session lease before any restore or RPC so a second session fails fast rather than driving sync against a shared checkpoint, then restores the persisted document before building the change pack, so the resume re-pushes the un-acknowledged changes rather than an empty pack. The initial presence is seeded only when no restore happened; a restore brings the presence map back with it, and seeding around it would either append a spurious local change or be silently overwritten. Three ways a resume can fail to reconcile, each surfaced as a localChangesDropped event carrying the discarded changes rather than losing them silently: - the stored bytes cannot be restored, either because the actor guard rejects a store reused under another identity (actor-mismatch) or because they cannot be decoded at all (restore-failed). The changes are recovered from the stored bytes, since the document that failed to restore never took them on; - the server compacted past the persisted epoch and rejects the resume (epoch-reanchor), so the document is re-anchored and the attach retried once as a fresh one; - the server purged the document while this client was offline and minted a fresh one, detected by a changed document id or a server sequence that regressed to 0 under a non-empty local snapshot (document-purged). The document id the guard compares against is now recorded on every attach and persisted with the envelope. Re-anchoring resets the change id along with everything else, so the actor is re-stamped at each reset site. Persisting is driven off every local change rather than the sync round trip: what this feature has to survive is precisely the case where no sync ever runs to completion, so a change is stored the moment it is made. The lease lives on the Attachment, so its lifetime is exactly the attachment's; an attach that fails before creating one releases it rather than locking the key out of every retry. * Add tests for the offline-persistence Client seams Covers what is reachable without a server: the MemoryDocStore lifecycle including concurrent access through its actor, NoopSessionLock's deliberate lack of coordination, a hand-rolled contended lock proving the refusal contract attach relies on, the Document.onLocalChange hook that drives persistence (fires on local changes, not on remote ones), the localChangesDropped event for each reason, and a full store round trip asserting the pending changes a resumed session would re-push survive toBytes/fromBytes intact. The private resume paths -- prepareOfflineResume, releasePersistence, the epoch-reanchor retry and the purge guard -- are only reachable through a real attach round trip, so they belong to the integration suite; the file's header records that rather than faking a server. * Close the offline-persistence gaps found in review Ten defects in the Client half, found by reviewing the port against yorkie-js-sdk#1338 rather than in isolation. Three lose user data on ordinary paths. A failed restore no longer resets the document. restoreFromBytes is all-or-nothing and throws before touching it, so there was nothing of the stored copy to undo -- but the document may hold edits the caller made before attaching, and the reset destroyed them. They were not even in the dropped payload, which is read from the stored bytes. Sync persists again after applying the response pack. A push that is merely acked advances the checkpoint and drains localChanges without appending one, so the local-change hook never fires; the stored envelope kept an already-pushed change under a stale checkpoint until the next edit, and a resume re-pushed it. Upstream persists in both places for this reason; replacing one with the other was wrong. The in-flight marker is set before the first suspension point. It sat after the offline-resume preamble, which really suspends on acquiring the lease and loading the store, leaving a window where a second attach of the same key saw both containers empty, passed the guard and reached the server -- where the duplicate surfaces as a misleading ErrClientNotFound that deactivates the whole client, the failure js#1337 exists to prevent. Also: deactivate releases every held lease, which otherwise outlived the client and locked the document key out of every later session under a cross-process lock; persists for one document are chained so two writes cannot land out of order and leave the store holding the older envelope; the epoch re-anchor retry seeds the initial presence the resume path skips, without which the client attached with no presence entry and was invisible to peers; restore failures are classified by error code rather than error type, since every envelope decode failure is a YorkieError too and a truncated file was reported as someone else's store; the envelope decoder tolerates trailing blobs it does not know, so an app downgrade cannot discard un-pushed edits; and the lease is released when seeding the initial presence throws. The dropped-changes payload is now a public DroppedChange projection. Change's fields are internal, so an app handling the data-loss event could read nothing from it -- defeating the purpose of carrying the changes at all. Adds an integration suite covering what only a server can prove: that a resumed document's un-pushed changes reach the server, that the lease and stored copy are torn down on each path that ends an attachment, and that concurrent duplicate attach is rejected locally. * Cover the unusable-stored-copy path end to end The failed-restore branch had no test. Mutation-checked: restoring the reset that was removed makes this fail with the caller's edit gone from both the local document and the server, which is the defect it pins. * Align the tests with the stable actor, and cover the review fixes The stable actor is not just a new identifier: it changes how concurrent edits are resolved. The actor used to be the session id, a time-ordered ObjectID, so the second-created client always held the higher actor and won every tie -- an outcome the integration tests encode throughout. A stable actor is derived from the client key, so creation order no longer implies actor order. withTwoClientsAndDocuments now pins c1 to the lower actor and c2 to the higher, keeping the tie-break deterministic, as upstream does in its own helper. With that, the version-vector and presence assertions move from the session id to the actor, since that is what keys the version vector, the presence map and the peer ids in watched events now. This mirrors the test changes that ship with yorkie-js-sdk#1338. Note for callers: peers are keyed by the stable actor, so an app that used Client.id to find itself among getPresences() must use getActorID(). This is a behavioural change in a public API. Adds coverage for the review fixes: envelope tolerance of trailing blobs in both directions, the DroppedChange projection field by field, the actor-mismatch and corrupt-envelope error codes pinned together, and the persist chain driven through Client's own enqueuePersist rather than a reimplementation of it -- removing the chaining makes that test fail, which a mirror of the algorithm could never have shown. The offline persistence helpers are internal rather than private so the test can reach them. The epoch-reanchor branch stays uncovered by unit tests: activate makes a real RPC that cannot be mocked into success, so there is no seam to drive attach without a server. The integration suite covers it instead. * Note the stable-actor behavioural change in the changelog * Pass the actor to the remaining version-vector assertions Three call sites wrote `client.id` without a force-unwrap, so the earlier substitution missed them and they kept passing the session id into maxVectorOf. The version vector then carried no entry for the document's actor and garbageCollect collected nothing -- a silent no-op rather than a failure, which is why it read as "0 is not equal to 3". * Close the teardown gaps found in the second review A client torn down internally kept its session leases. When the server no longer knows a client, handleConnectError deactivates it in place rather than through deactivate(), and that path dropped the attachments -- and with them the only reference to each lease -- without releasing them. deactivate() could not clean up afterwards either, since it returns early once the status is already deactivated. Under a cross-process lock the document key was then locked out for the lifetime of the process. Teardown is now three phases rather than two: quiesce, remove, release. Detach queues a persist of its own by clearing presence, so removing the stored copy before that write had drained let the save land after the removal and resurrect the envelope. Simply removing after releasing would have reopened the other side of it -- another session resuming a copy that is about to be deleted -- so the two are separated by the drain. The stable actor is adopted only when it is a usable ActorID. Every ticket and change id encodes it through String.toData, which yields nil for anything that is not even-length hex and is then written as an empty actor with no error, collapsing tie-breaks and keying the version vector on "". A server sending something unexpected now falls back to the session id with a warning rather than silently corrupting every change. Also: restoreFromBytes, getPendingChangeStructs and resetForReanchor were public by accident of living in a public extension, and are internal mechanisms rather than API; a presence-seeding failure on the re-anchor retry is logged rather than swallowed, since the same input fails loudly on a normal attach; and the sync-path persist is gated on the attachment so it cannot outlive it. The lease-leak test injects ErrClientNotFound through the service mocking seam so it reaches handleConnectError with a lease actually held -- an earlier version passed with the fix reverted, because deactivate() had already released the lease and nothing was left to leak. * Set the changelog date to the PR-creation date * Address the CodeRabbit review A failed attach left everything it had taken. Once the attachment owns the session lease, the catch block released nothing, so a throw in the watch loop, the initialization wait or the initialRoot update left the attachment registered, the lease held and the document persisting behind an attach the caller saw fail. rollBackFailedAttach undoes all three. A storage read failure is no longer indistinguishable from an absent key. `try?` turned a transient read error into "nothing stored", so the attach went ahead fresh and the first local change overwrote the very envelope that could not be read. The attach still proceeds -- failing it outright would make a document unusable over a read glitch -- but the session does not persist, so whatever is stored survives for a session that can read it. A failed removal of the persisted copy is logged rather than swallowed. The lease is still released: SessionLock has no recovery API, so holding it until removal succeeds would strand it, locking the document key out for the lifetime of the process, which is worse than an envelope outliving its detach -- and a stale envelope is re-validated on the next resume by the actor guard, the epoch check and the purge guard. The change id now advances before the local-change hook fires. Not reachable today, since Document is main-actor isolated and the mutation is synchronous, so the task the hook starts cannot run until it returns; the ordering should not depend on that reasoning holding. test_can_handle_concurrent_attach_with_initialRoot builds its two clients directly rather than through the helper, so it never got the actor ordering swap and its expected winner became a coin flip under the stable actor. It passed the previous run by luck. Pinned the way upstream pins the same test. * Close the outside-diff findings from the CodeRabbit review The previous commit's canPersist fix was incomplete. It skipped the local-change hook when the store could not be read, but syncInternal persists on its own and was gated only on the store and a live attachment, so a successful sync still overwrote the envelope this session had deliberately left alone. The decision now rides on the attachment as persistsToStore and gates every enqueuePersist. remove() never released the session lease. detachInternal clears attachmentMap itself, so the attachment looked up after it is nil and the lease it held was never released -- locking the document key out of every later session under a cross-process lock. It is now captured before detachInternal, as the detach path already did. This is the second teardown path on this branch to leak a lease the same way, after handleConnectError. Both come from cleanup living in the callers of detachInternal rather than inside it, which is where upstream keeps it; the iOS split exists because detachInternal is synchronous and the release is async, but it has now cost two bugs. test_can_handle_concurrent_attach_with_initialRoot read doc2 where it meant doc1 in its final step, so it never actually asserted that user1's document accepts the higher actor's root after syncing.

Commit:fe47a69
Author:longhoang-lewy
Committer:GitHub

Apply updates from yorkie-js-sdk 0.7.16 (#270) * Keep a tree position resolvable when two nodes claim one ID * Stamp merge-bound inserts and filter style-range interlopers * Stop undo and element splits from reusing a node's identity Adds Operation.TreeEdit.split_tickets (field 11) to the wire, so a replica applying an element split consumes the tickets the originating replica issued instead of reconstructing them. The iOS reconstruction fallback also re-added the content count on every call, so a multi-level split produced non-consecutive delimiters that diverged from the JS SDK and the server. The base is now captured once and advanced one delimiter per issued ticket, matching upstream. * Bump version to 0.7.16 and pin CI yorkie server to v0.7.16 * Address review: propagate position-resolution errors from style and edit `mergedAnchorInterloperGuard` and the merge-bound insert stamp swallowed a failing `toTreeNodePair` with `try?`, silently disabling the §9.4 guard or leaving an insert unstamped where the JS SDK aborts the operation. Both call sites already throw, so propagate instead — a silent cross-SDK divergence is worse than a visible error. Also drops the `offset == size` half of the splitText early return, which the new range guard already covers. * Add tests for v0.7.16 tree node identity, split tickets and merged style anchors * Note the v0.7.16 server requirement in the changelog * Extract tree edit and style helpers to satisfy the lint gate The v0.7.16 port pushed CRDTTree.edit, CRDTTree.style and TreeEditOperation.execute past the 110-line function_body_length limit, and left the split-ticket TODO block orphaned from its declaration. Pulls out resolveStyleRange, intendedMergeStamp, recordInsertedContent and makeSplitTicketIssuer. Behaviour is unchanged: 661 unit tests and the full integration suite against a v0.7.16 server pass exactly as before. * Port the remaining v0.7.16 unit tests for merge-stamp propagation Adds the two tests from yorkie-js-sdk#1317 that were missed: splitElement carrying mergedFrom/mergedAt onto the split product, and an insert declared inside a merged-away parent being stamped with the declared parent. Both were mutation-checked: reverting the cloneElement merge stamp fails the first. The second's mergedAt assertion does not discriminate the sibling-ticket lookup on iOS, because CRDTTreeNode.remove keeps the earliest tombstone where the JS SDK overwrites with the newer one; that divergence predates this sync and is noted in the test.

Commit:316dce9
Author:longhoang-lewy
Committer:GitHub

Apply updates from yorkie-js-sdk 0.7.14 (#268) * Move tombstones with merge to preserve tree RGA anchors A merge collected only live children, so tombstoned children were left behind in the merge source. A concurrent insert that referenced one of those tombstones as its RGA anchor then resolved against a node the merging replica no longer had in the merge target, and the two replicas ordered the insert differently. Collect allChildren (innerChildren) instead, and move tombstones along with the live ones so they survive as RGA anchors in the merge target, where the RGA tie-break orders the concurrent insert consistently. Moving a tombstone with detachChild + append would corrupt the index, because detachChild unconditionally subtracts the child's paddedSize from the old ancestors even though a removed node contributes nothing. Add IndexTreeNode. moveChild, which re-parents with size accounting only for live children. iOS caches just the visible size and derives the include-removed total on demand, so unlike yorkie-js-sdk only that one dimension is relocated. Nodes without a parent are now skipped rather than appended untracked: without mergedFrom they are invisible to the merge-delete propagation and rebuild. * Flatten chained merge to converge an anchored concurrent insert When a merge lands on a parent that a prior concurrent merge had already merged away, the children piled up under the removed intermediate, so the chain P->Q->R stayed nested. rebuildMergeState can only ever represent the compressed chain — it records one mergedFrom pointer per child and reads the child's final physical parent — so runtime state and snapshot-derived state disagreed, and replicas that applied the two merges in different orders diverged. Resolve the destination through the new resolveMergeTarget, which follows the mergedInto chain past merge-away tombstones (with a seen set guarding against a concurrent mutual merge), and move children to that final target instead. mergedFrom/mergedAt are now stamped only on a child's first move, so a child carried through a chained merge keeps its original source and the original merge ticket; stamping on every move would record a different ticket depending on the order the merges were applied. mergedInto is now derived from moved children rather than the merge-source list, matching what rebuildMergeState can reconstruct, and the delete propagation compares against the resolved destination rather than fromParent. * Keep style ranges from crossing a concurrent merge anchor A style range boundary inside a merged-away parent resolved through the insertion boundary, which sits before the first child the merge moved. That extended the range over nodes a peer had concurrently inserted between the merge-source tombstone and those moved children — nodes the styling client saw outside its range, because at the time it styled they sat after the then-live parent. Add a boundary mode to findNodesAndSplitText. A range boundary now resolves to the position right after the merge-source tombstone, so the range neither grows over nor shrinks past nodes inserted at that anchor. style and removeStyle pass .range; edit keeps the insertion boundary, where RGA ordering must break ties. * Add identity-preserving restore for Tree undo/redo Reverse a Tree edit by reviving the nodes it removed and re-removing the nodes it inserted, both by ORIGINAL identity, instead of copy-reinserting content. Concurrent undos then converge, because both replicas act on the same nodes rather than each inserting a fresh copy. edit() captures one TreeRestoreSpan per node it transitions visible to tombstoned, plus one per node it inserts, and returns them only when the edit was merge- and split-free; otherwise the spans do not fully describe the deletion and the op layer keeps the copy-reinsert reverse. CRDTTree gains restore/retombstone and a recreate path that rebuilds a GC-purged node under its original id, anchoring it by same-insertion neighbour, then the captured boundary siblings, then a deterministic id-order fallback so every replica lands it in the same slot. These live in their own extension to keep the type within its length budget. compareNodeID is not ported: CRDTTreeNodeID is already Comparable on (createdAt, offset), the same total order the fallback needs. The guarded insPrev/insNext derefs in the upstream change were already optional-chained here. * Bump CI yorkie server to v0.7.14 TreeEdit.restore_spans/restore_mode/retombstone_spans (fields 8-10) only exist from server v0.7.14. The server re-encodes operations from its own domain model rather than relaying bytes, so a v0.7.13 server silently drops those fields: an identity-preserving Tree undo applies on the originating replica and reaches peers stripped, leaving them diverged. Pinned to the bottle commit rather than the version bump, whose bottle block is a placeholder that fails to pour. * Recreate purged text runs in order on restore, not reversed When every character of a run is its own insertion, none of the same-insertion anchor rungs fire during restore, so each recreated fragment fell through to the same fixed fallback anchor and prepended there — rebuilding the run reversed ("my name" came back as "eman ym"). Thread a chain anchor through restore: the last node placed at the current cursor, in document order. A fragment with no surviving same-insertion anchor now chains after it, keeping the run left-to-right. Added as rung (d), between the rightmost-piece rule and the operation's fallback anchor. This only reproduced once the tombstones were GC-purged; un-tombstoning in place already preserved order. * Add tests for v0.7.14 restore ordering and int32 promotion parity TextRestoreAfterGCTests ports the upstream regression: type a string character by character, delete a run, purge the tombstones, then undo — and assert the run returns forward rather than reversed. Verified it fails without the fix, producing "hello eman ym is". PrimitiveTests gains a parity guard for yorkie-js-sdk#1291. That change promotes out-of-int32 integers to Long because JS has a single number type and classified any integer as Integer, overflowing int32 on the wire. Swift cannot express the bug — every .integer construction is fed by a statically-typed Int32, and Int maps to .long — so there is no logic to port. The test pins the property that makes it unrepresentable. * Bump version to 0.7.14 and add the CHANGELOG entry Entries mirror the yorkie-js-sdk v0.7.14 changelog wording, one line per upstream change. The iOS PR links are backfilled once the PRs exist, so the links point at the PR that actually carries each change rather than a number guessed ahead of time. * Slice recreated tree text in UTF-16 with an exclusive end recreateFromSpan built the recreated fragment with String.substring(from:to:), which treats `to` as inclusive and indexes by grapheme. Tree text offsets are UTF-16 throughout, so the slice ran one character too long and, for a span containing an emoji, could return empty. The bug was masked whenever the recreated gap reached the end of the span, because the inclusive end clamps to count - 1 — so a whole-span undo looked correct. On a partial gap it produced a node whose value and size disagreed with the JS SDK, corrupting every ancestor's index accounting and overlapping the id range of a surviving piece, silently and without a throw. Slice the NSString directly with an exclusive NSRange and guard the bounds. Also add the tests the feature shipped without: restore/retombstone identity and idempotence, the moveChild size accounting in both dimensions (the one place the port deliberately diverges from JS), and a TreeRestoreSpan converter round-trip covering a tombstoned attribute and each malformed-timestamp rejection. * Backfill the PR links in the v0.7.14 CHANGELOG entry

Commit:a1396b0
Author:Lewy

Add identity-preserving restore for Tree undo/redo Reverse a Tree edit by reviving the nodes it removed and re-removing the nodes it inserted, both by ORIGINAL identity, instead of copy-reinserting content. Concurrent undos then converge, because both replicas act on the same nodes rather than each inserting a fresh copy. edit() captures one TreeRestoreSpan per node it transitions visible to tombstoned, plus one per node it inserts, and returns them only when the edit was merge- and split-free; otherwise the spans do not fully describe the deletion and the op layer keeps the copy-reinsert reverse. CRDTTree gains restore/retombstone and a recreate path that rebuilds a GC-purged node under its original id, anchoring it by same-insertion neighbour, then the captured boundary siblings, then a deterministic id-order fallback so every replica lands it in the same slot. These live in their own extension to keep the type within its length budget. compareNodeID is not ported: CRDTTreeNodeID is already Comparable on (createdAt, offset), the same total order the fallback needs. The guarded insPrev/insNext derefs in the upstream change were already optional-chained here.

Commit:f83b39e
Author:longhoang-lewy
Committer:GitHub

Apply updates from yorkie-js-sdk 0.7.13 (#267) * Prevent panic when splitLevel walks past the tree root * Register GC pairs for pieces split off tombstoned nodes * Identity-preserving restore for Text undo/redo * Bump version to 0.7.13 and pin CI yorkie server to v0.7.13 * Add tests for v0.7.13 identity-preserving restore and GC split leaks * Address review: carry attributes on restored text, validate RestoreSpan from the wire * Resolve head-anchored reverse-edit range when restore fields are stripped * Clamp RestoreSpan bounds from the wire instead of throwing yorkie-js-sdk's fromPbSpan never rejects a span, so throwing here diverged from upstream and could wedge applyChangePack: the error escapes the sync loop, and the change pack re-pulls and re-fails indefinitely. Derive the span bounds from content length and clamp a negative start, which keeps the NSString slicing in restore() in bounds without rejecting the op. A nonconforming peer now degrades to a harmless no-op. * Cap RestoreSpan start so the derived end cannot overflow The clamp derived end as start + content length, and Swift's + traps on Int32 overflow. A wire span with a very large start therefore terminated the process instead of degrading to a harmless no-op, which is the opposite of what the clamp was for. Cap start at Int32.max - length so the derived span always stays representable, and cover the boundary with a regression test.

Commit:fa2e042
Author:Lewy

Identity-preserving restore for Text undo/redo

Commit:3e87069
Author:longhoang-lewy
Committer:GitHub

Apply updates from yorkie-js-sdk 0.7.12 (#266) * Add disablePresence option for presence-free Documents * docs: add CHANGELOG entry for v0.7.12 * chore: bump version to 0.7.12 and CI yorkie server pin * test: add disablePresence unit + integration coverage for 0.7.12 sync

Commit:c2f51ca
Author:Lewy
Committer:Lewy

Add disablePresence option for presence-free Documents

Commit:523a607
Author:longhoang-lewy
Committer:GitHub

Apply updates from yorkie-js-sdk 0.7.10 (#263) * fix: deselect editor selection when undo/redo changes text In the RichTextEditor example, undoing/redoing a text edit (vs a style-only change) shifts the characters under the current selection, so the kept NSRange highlighted the wrong text (e.g. select "SDK", undo "SDK"->"SD" left "SD " selected). undo()/redo() now compare the document's plain text before/after and, when it changed, collapse the selection to a caret — mirroring Android, which deselects in the same case. Style-only undos leave the text unchanged and keep the selection. * fix: correct RichTextEditor remote-collaboration rendering - apply remote un-strike (and other style removals) by decoding a non-"true" attribute value as the unset style instead of ignoring it - identify peers by clientID rather than username so the participant list and cursors no longer treat a peer sharing the local display name as self - preserve the local scroll position on remote-driven re-renders (remote text, presence/cursor, watched/unwatched) so a peer's edit or selection no longer jumps the viewport to the last line; local edits still follow the caret * fix: collapse selection to caret after deleting a selected range Deleting a selected range left RTUITextField re-applying the now-stale multi-char selection (a UITextRange from the pre-delete, longer text) onto the shortened text, highlighting the wrong span. Collapse the selection to a caret at the end of the deleted range so updateUIView's backspace-style length subtraction lands the caret at the selection start (e.g. deleting 8...10 leaves the caret at 8). * fix: sync formatting toolbar state when applying a style Applying a style via a toolbar button did not update isBold/isItalic/ isUnderline/isStrikethrough, so the button only reflected the change on the next selection change ("reselect to activate"). Because the state stayed stale, the next tap re-sent the same value (e.g. bold:true on already-bold text), producing a same-value no-op style op that polluted the undo/redo stack (undo appearing to do nothing) and synced needlessly. Reflect the applied value on the toolbar immediately so toggling works and no no-op style ops are created. * Add disableGC attach option * Channel lifecycle via RefreshChannel only * chore: bump to 0.7.10 — changelog, version, CI yorkie-server pin * refactor: move Client channel methods into an extension The channel lifecycle methods (attachChannel/detachChannel/refreshChannel/ heartbeat/syncChannel) grew with the RefreshChannel-only consolidation, pushing the Client class body to ~999 lines against SwiftLint's 1000-line type_body_length limit. Relocate them verbatim into an extension Client (no behavior change; same-file extension retains private member access) to bring the class body to ~861 and restore margin. * docs: backfill 0.7.10 CHANGELOG PR link * test: rename single-char client variable to satisfy SwiftLint identifier_name * style: collapse double blank line at extension boundary (SwiftLint vertical_whitespace) * style: single-line Attachment init to satisfy SwiftFormat indent * test: cover channel-method guards and error-recovery branches Add integration tests for the previously-uncovered guard and error/recovery paths in Client's channel methods (attach/detach/sync not-active/not-detached/ not-attached guards, attachChannel first-call-failure rollback, and refreshChannel ErrSessionNotFound recovery), lifting Sources/Core/Client.swift patch coverage above the 75% bar.

Commit:cad0ae8
Author:longhoang-lewy
Committer:GitHub

Apply updates from yorkie-js-sdk 0.7.9 (#262) * feat(devtools): add cross-platform CRDT debugger Adds an opt-in devtools layer for diagnosing where iOS / Android / JS diverge during sync, by capturing the exact data a client exchanges with the server in the yorkie-js-sdk devtools format. - Sources/Devtools: an always-on ring-buffer recorder that encodes every replayable DocEvent to byte-compatible Array<DocEventsForReplay> JSON, plus iOS-only connection / sync-status diagnostics. Document gains an enableDevtools option, dumpDevtools/exportDevtools, and a publish() tap. - Sources/DevtoolsUI: an in-app SwiftUI inspector (document tree + change timeline) reading the recorder live. - Sources/DevtoolsServer: a debug-only loopback/LAN HTTP server that serves a self-contained browser viewer at / and the live buffer at /events, so a browser shows changes in real time and can diff two recordings to locate the first divergent operation. - Tools: standalone sample recordings and usage docs. - Examples/RichTextEditor: wires both inspectors in and moves undo/redo ahead of the B/I/U/S formatting buttons. * Fix Polling channel subscriber notification Port of yorkie-js-sdk#1247. On a channel refresh (polling), publish a presence-changed event only when the session count actually changed, mirroring the JS RefreshChannel path. The 'expose polling via React' part of the upstream PR has no iOS counterpart. * Add PeekChannel client Port of yorkie-js-sdk#1256. Adds the PeekChannel RPC (proto + regenerated Connect/protobuf), a YorkieService wrapper, and Client.peekChannel(_:) which reads a channel's session count without creating a server session, heartbeats, or event subscription. The usePeekChannel React hook from the upstream PR has no iOS counterpart. * chore: bump version and changelog to 0.7.9 * chore: bump CI yorkie server to v0.7.9 * test: port tests for PeekChannel and polling channel notification Port of channel_polling_test.ts at v0.7.9 covering yorkie-js-sdk#1256 (Add PeekChannel client) and yorkie-js-sdk#1247 (Fix Polling channel subscriber notification). Five tests in Tests/Integration/ChannelPollingTests.swift: - peekChannel returns session count without creating a session - peekChannel reflects increased count when a second client attaches - peekChannel throws errClientNotActivated when client is inactive - refreshChannel publishes presenceChanged when count increases (PR#1247) - refreshChannel does NOT publish presenceChanged when count is unchanged The two server-side peekChannel tests skip automatically with XCTSkip when the tunneled server predates v0.7.9 (unimplemented RPC). All five tests compile and run cleanly; three pass, two skip on the current tunnel. * Add live room member counts to Channel example via peekChannel * fix: satisfy CI-pinned SwiftFormat hoistTry and SwiftLint identifier_name in devtools tests * fix: move Client.peekChannel into an extension to satisfy type_body_length The added method pushed the Client class body to 1007 lines, over the 1000-line SwiftLint type_body_length limit. Extension bodies are not counted, so peekChannel moves into a public extension Client with no behavior change. * fix: extract devtools tap from Document.publish to satisfy cyclomatic_complexity The devtools recording tap added two branches to publish(_:), pushing its cyclomatic complexity to 26 (over the SwiftLint --strict warning threshold of 25). Move the tap into a recordForDevtools(_:) helper; no behavior change. * test: harden channel polling tests (unique keys, deterministic waits) - Replace second-resolution timestamp in channelTestKey with an 8-char UUID prefix to prevent key collisions across concurrent or rapidly-repeated runs. - Harden the "no event when count is unchanged" test with an inverted XCTestExpectation (1 s window) so late callbacks delivered on RunLoop.main are caught before the assertion fires, eliminating the race in the original plain-counter check. - Convert comment blocks preceding the class and helper declarations to /// doc comments to satisfy the SwiftFormat docComments rule. - Document why the line-58 defer teardown is not fragile (fire-and-forget deactivate, non-fatal by design; key uniqueness was the only real concern). * fix: address CodeRabbit review feedback - DevtoolsRecorder: replace single onUpdate closure with multicast addObserver/removeObserver so the browser server and SwiftUI inspector can observe concurrently without clobbering each other; fire on clear(). - Client.peekChannel: wrap RPC in do/catch + handleConnectError, matching attachChannel/detachChannel so auth/state recovery runs on failure. - DevtoolsServer/Resources/viewer.html: escape all recording-derived values before innerHTML; use null-prototype maps for path-tree segments. - DevtoolsUI/JSONNode: stable path-based id so OutlineGroup keeps expansion state across live refreshes (was a fresh UUID per build). - RichTextEditor example: gate devtools recording + LAN inspector behind #if DEBUG so release builds never record or expose document data. - Channel example: clear stale member count when peekChannel fails; add accessibility labels to icon-only undo/redo/inspector buttons. * test: use drain expectation to fix flaky unchanged-count polling test The "no event when count is unchanged" test was non-deterministically failing with "Fulfilled inverted expectation". Root cause: `attachChannel` unconditionally starts a Watch stream. The stream delivers a `ChannelPresenceEvent(type: .initialized, ...)` via `handleWatchChannelResponse` asynchronously after `attachChannel` returns. Because the stream dispatch is async (Task + RunLoop.main), this event can arrive during the 1 s inverted-expectation window that was installed immediately after `syncChannel` — firing the callback and tripping the inverted expectation even though `refreshChannel` correctly suppressed its own event (the PR #1247 fix is working correctly). Note: `Channel.isRealtime` only controls the heartbeat timer; it does NOT prevent the watch stream from starting, so `isRealtime: false` alone would not close the race. Fix: install a drain expectation before the first `syncChannel` and wait for it to be fulfilled (via `fulfillment(of:timeout:5.0)`) before swapping in the inverted "no event" expectation. This guarantees that both the refresh RPC path and the watch-stream initialization path have completed before the observation window opens, so any event arriving in the 1 s window is genuinely spurious and correctly fails the test. * test: use addTeardownBlock for awaited channel test cleanup Replace defer { Task { try? await ... } } fire-and-forget cleanup with addTeardownBlock, which XCTest awaits before the test completes — so client deactivation/detachment finishes before the next test runs instead of leaking activated clients across tests. Per CodeRabbit review. * fix: address CodeRabbit follow-ups (observer snapshot, JSONNode id escaping) - DevtoolsRecorder.notifyObservers snapshots observers before iterating, so an observer callback that (un)registers cannot mutate the dictionary mid-iteration. - JSONNode escapes '/' and '\' in each path segment when building the stable id, so an object key containing '/' cannot collide with a deeper path. * chore: exclude devtools modules from code coverage Devtools (recorder, browser server, SwiftUI inspector) is opt-in debug tooling; exclude Sources/Devtools, Sources/DevtoolsServer, Sources/DevtoolsUI from Codecov so it doesn't drag the SDK's coverage metric.

Commit:25547d5
Author:longhoang-lewy
Committer:GitHub

Apply updates from yorkie-js-sdk 0.7.6 (#258) * Copy attributes to split node in cloneElement * Implement toJSON for Bytes and Date primitive types * Register LWW-losing element in GC set on Set conflict * Apply array-move-convergence with LWW position register Port the array-move convergence fix: each RGANode now carries an LWW position register (position_created_at/moved_at/removed_at in the proto) separate from the element, so concurrent moves converge by LWW on the position. moveAfter resolves the anchor by position identity and, on both win and loss, produces a dead position node returned for GC; MoveOperation carries position identity (via posCreatedAt) to stay wire-compatible with the Go server/JS. Dead position nodes serialize with position timestamps and no element. CRDTRoot's GC walk registers dead position nodes. Ports yorkie-js-sdk#1227. * Unify presence event emission with reconcilePresence * test: cover array-move LWW, primitive toJSON, cloneElement attrs, GC, presence Port the v0.7.6 test scenarios: array-move LWW position register and move convergence (RGATreeList), primitive toJSON for Bytes/Date, split node attribute copy, LWW-losing element GC on Set conflict, and the reconcilePresence event emission. Integration cases need a live yorkie server v0.7.6. * chore: bump version to 0.7.6 and update changelog * chore: bump CI yorkie server to v0.7.6 * test: expect base64 bytes in various-types after toJSON change #1225 changed Primitive.toJSON to emit Bytes as base64 (JS-faithful), so the pre-existing test_can_support_various_types must expect "AQI=" for [1,2] instead of the old decimal-array "[1,2]". Drop the now-resolved bytes/date TODO. * Fix array index corruption when appending after a tail move moveAfter's winner path deleted the old (now-dead) position node from the index splay tree while `last` still pointed at it. A subsequent insert/append that anchors on `last` (e.g. append after moving the tail element to the front) then failed to locate its anchor in the index tree, so the appended element was never indexed — get(index:) returned nil and crashed callers. Keep the dead position node in the splay tree with length 0 (re-splay to refresh weights) instead of deleting it, matching the LWW-loser path and JS, which keep dead position nodes splayed until GC purge. * Fix array move-node encoding and deepcopy to preserve positions - Converter.toRGANodes: encode a moved node's positionMovedAt from the actual move ticket (entry.posMovedAt), not positionCreatedAt, and branch on posMovedAt like JS toRGANodes — keeps the move timestamp on the wire. - CRDTArray.deepcopy: iterate all position nodes and reconstruct moved (addMovedElement) and dead (addDeadPosition) slots with their position timestamps, mirroring JS CRDTArray.deepcopy. Copying only live nodes dropped moved/dead positions, so a move on the clone could anchor after a wrong or GC'd position.

Commit:9e8e138
Author:longhoang-lewy
Committer:GitHub

Apply updates from yorkie-js-sdk 0.7.5 (#257) * Add Counter dedup mode with HyperLogLog for UV measurement Port the dedup Counter feature: a new HyperLogLog (HLL.swift, xxhash64 — byte-compatible with the Go server) backs a dedup mode on CRDTCounter (Int32-valued, value = HLL cardinality estimate). Adds the public JSONDedupCounter authoring type (wired through JSONObject/JSONArray), IncreaseOperation gains an `actor` field (dedup increases require an actor and are not undoable), and the converter/proto carry the new VALUE_TYPE_INTEGER_DEDUP_CNT, Counter.hll_registers, and Increase.actor. Also folds in "Simplify Counter interface and replace long with bigint" (yorkie-js-sdk#1216): iOS already uses native Int32/Int64 (no Long library), so only the interface shape is mirrored — the bigint change is a no-op here. Skipped upstream yorkie-js-sdk#1218 (default transport gRPC-Web -> Connect): iOS already uses connect-swift's Connect protocol, so there is nothing to switch. Ports yorkie-js-sdk#1215 and #1216. * Enable splitLevel=1 concurrent tests and add split undo/redo Port split undo/redo: a split's reverse is a boundary-deletion that merges the split elements back (TreeEditOperation gains redoSplitLevel + toSplitReverseOperation, so the boundary-deletion's own reverse re-splits). IndexTree.splitElement now excludes removed children from the (single, visible) size, matching the upstream visibleSize fix. Ports yorkie-js-sdk#1219. * Add undo/redo support for TreeStyleOperation CRDTTree.style()/removeStyle() now also return the previous attribute values (and, for style, the keys that were newly added) captured from the first styled node; TreeStyleOperation builds a reverse op from them (combined / removeStyle / style). Callers in JSONTree updated to the new return arity. CRDTTree also carries the #1219 split-loop tweak (findOffset(includeRemoved: true)) since it lives in the same file. Ports yorkie-js-sdk#1221. * test: cover Counter dedup/HLL and Tree split/style undo Add HLL structural tests, CRDT dedup-counter + JSONDedupCounter unit tests, and integration suites for Counter dedup convergence, Tree split-level-1 undo/redo, and TreeStyle undo/redo (incl. overlapping-undo correctness), mirroring the yorkie-js-sdk v0.7.5 test scenarios. Integration cases require a live yorkie server v0.7.5. * chore: bump version to 0.7.5 and update changelog * chore: bump CI yorkie server to v0.7.5 * Fix tree style undo capturing tombstoned attribute values style()/removeStyle() captured previous attributes via getNodeByKey(key)?.value, which returns a tombstoned (removed) attribute's value. After a removeBold, the bold key is tombstoned but was still read as an existing attribute, so the reverse op restored the stale value instead of removing the key — undo/redo of style removal left the attribute in place. Gate the capture on RHT.has(key:) (non-removed) to match yorkie-js-sdk, so a tombstoned attribute counts as absent and the reverse op removes it. * Log instead of silently dropping HLL restore failure on deepcopy deepcopy() used `try? copy.restoreHLL(bytes)`, silently swallowing any error and producing a dedup-counter copy with an empty HLL but the original's timestamps. hllBytes() always yields a valid 16384-byte payload so this cannot happen for valid state, but a future invariant break should be loud: log the error via Logger.error rather than dropping it. (Avoids try!, per repo style.)

Commit:65cd1b8
Author:longhoang-lewy
Committer:GitHub

Apply updates from yorkie-js-sdk 0.7.4 (#256) * Mirror tree merge snapshot encoding fix from Go SDK Add `merged_from` (field 9, TreeNodeID) and `merged_at` (field 10, TimeTicket) to the TreeNode proto so a node moved by a concurrent merge carries the merge witness and its immutable merge ticket in the snapshot encoding. `merged_at` is stored alongside `merged_from` because the source parent's `removed_at` may be overwritten by later LWW tombstones and cannot serve as the merge-time causal boundary. Regenerated resources.pb.swift via buf (pinned plugins). Ports yorkie-js-sdk#1210. * Fix convergence bugs in concurrent tree merge/split Port the v0.7.4 Tree merge/split convergence fixes. Because the upstream changes are tightly interleaved across tree.ts / index_tree.ts (later fixes build on the mergedFrom/mergedAt fields introduced mid-series), they are ported as one cohesive change rather than reconstructed per-PR: - ticketKnown() causal-visibility helper; toBeMergedNodes gating so elements created by concurrent ops are not merged (Fix 9). - detachChild() to move alive nodes between parents (vs removeChild which now silently skips an already-detached child during GC purge). - mergedFrom/mergedAt/mergedInto on CRDTTreeNode; rebuildMergeState() on snapshot load reconstructs the mergedInto cache from persisted mergedFrom; converter round-trips mergedFrom/mergedAt. - findNodesAndSplitText redirect: insertion into a merge-tombstoned parent forwards to the merge target. - advancePastUnknownSplitSiblings / hasUnknownSplitSibling: edit and style ranges advance past concurrent split siblings; style/removeStyle skip End-token styling when an unknown split sibling extended the range. - edit() cascade-deletes unknown split siblings and propagates deletes to children moved by prior merges; remove() now returns Bool to gate GC pairs; split()/splitElement take a versionVector and keep merge-moved children whose source is local in the left node (Fix 8, via the shouldStayLeftOnSplit IndexTreeNode hook so the generic tree stays free of CRDT types). - traverseInPosRange returns early on an empty (fromIdx > toIdx) range. Ports yorkie-js-sdk#1202, #1203, #1204, #1205, #1206, #1211 (and the #1210 encoding consumed here). Intentional iOS divergences: the write-only `preTombstoned` set from the JS source is omitted (dead code); remove() keeps iOS's existing LWW tombstone direction (only the Bool return is added) — both tracked as follow-ups. * test: cover tree merge/split convergence and mergedFrom/mergedAt encoding Port the v0.7.4 tree convergence tests: converter mergedFrom/mergedAt round-trip and splitText/deepcopy merge-metadata unit tests, plus the concurrent merge/split convergence integration scenarios (overlapping, contained, and side-by-side merges) mirroring yorkie-js-sdk integration/tree_test.ts. Integration cases require a live yorkie server v0.7.4. * chore: bump version to 0.7.4 and update changelog * chore: bump CI yorkie server to v0.7.4 * Fix RichTextEditor undo/redo not re-rendering after revert document.undo()/redo() revert the CRDT correctly, but the editor never repainted from the reverted document: syncTextSnapShot() reused the on-screen attributed string when the cached `content` matched the doc text (so a content revert re-applied the stale string) and early-returned on the didFinishSync guard when the plain string was unchanged (so a style-only revert never rendered). Add a `force` path to syncTextSnapShot() that always rebuilds the attributed string from the document and skips both shortcuts; undo()/redo() now call it with force: true. Normal typing keeps the optimized path. * chore: extract tree edit helpers to satisfy SwiftLint Split the merge-move, cascade-delete-split-sibling, and propagate-deletes-to-merged-children blocks out of edit() into private helpers (applyMergeMoves, collectUnknownSplitSiblings, propagateDeletesToMergedChildren) to bring edit() under the cyclomatic-complexity and function-body-length limits. No behaviour change. * Fix tombstone-inclusive tree traversal for concurrent merge+delete convergence The overlapping-merge-and-delete-element-node convergence case diverged: after a client merged two paragraphs, a concurrent delete of the second paragraph's content from another client was not propagated, leaving the merged-in text alive ("<p>ab</p>" instead of "<p>a</p>"). Root cause: edit()'s range traversal excluded tombstoned nodes, so the merge-source parent (tombstoned by the local merge) was never visited and propagateDeletesToMergedChildren never fired. yorkie-js-sdk threads `includeRemoved=true` from edit() through the range walk; iOS lacked the parameter entirely. Thread `includeRemoved` (default false) through findOffset, tokensBetween, indexOf, addSizeOfLeftSiblings, toTreePos, toIndex and traverseInPosRange, and pass true from edit() only (style/removeStyle keep the visible-only walk, matching JS). iOS keeps a single visible `size`, so the JS totalSize notion is computed on the fly via nodeLength(includeRemoved:)/ paddedLength(includeRemoved:). * test: harden merge-state assertions and document shared-ops convergence test Address PR review feedback: - ConverterTests: assert merge-link identity (movedChild.mergedFrom == sourceParent.id, sourceParent.mergedInto == firstP.id) instead of only non-nil, so the roundtrip regression can't pass with wrong links. - TreeMergeConvergenceTests: document that multi_level_split_with_concurrent_merge_and_text_split intentionally shares its setup/operations with the cascade-delete test (mirroring the upstream pair 1:1) and asserts convergence-only by design.

Commit:fe574cc
Author:longhoang-lewy
Committer:GitHub

Apply updates from yorkie-js-sdk 0.7.1 (#253) * Retain peer presence across watch stream reconnections * Add tree-level schema validation and ProseMirror example Ports the tree-level schema validation from yorkie-js-sdk#1181: a TreeNodeRule (node_type/content/marks/group) in the ruleset proto, a ProseMirror-compatible content-expression parser/matcher, and a tree validator wired into the yorkie.Tree ruleset check. The ProseMirror example and ANTLR schema grammar are JS-only (the iOS SDK consumes pre-parsed rulesets via proto) and are not ported. * test: cover tree-level schema validation and presence retention * chore: bump CI yorkie server to v0.7.1 * chore: bump version to 0.7.1 and update changelog * test: assert CreateSchema status; drop ProseMirror from changelog note

Commit:d7c8aff
Author:longhoang-lewy
Committer:blurfx

Apply updates from yorkie-js-sdk 0.6.49 (#251)

Commit:6135265
Author:longhoang-lewy
Committer:GitHub

Apply updates from yorkie-js-sdk 0.6.49 (#251) * Add undo/redo support for Text.Style operations * Return unwrapped primitive values from Array iterator * chore: bump version to 0.6.49 and update changelog * test: cover mixed style-undo reverse op and heterogeneous Array elements()

Commit:b0f6b85
Author:longhoang-lewy
Committer:GitHub

Apply updates from yorkie-js-sdk 0.6.48 (#250) * Replace WatchDocument and WatchChannel with unified Watch RPC Also extracts the shared watch stream lifecycle (yorkie-js-sdk#1160); iOS already centralizes the watch lifecycle in doWatchLoop/runWatchLoop, so no separate helper is introduced. * Remove --backend-disable-webhook-validation from docker-compose * chore: bump version to 0.6.48 and update changelog

Commit:7ae8d0b
Author:longhoang-lewy
Committer:GitHub

Apply updates from yorkie-js-sdk 0.6.47 (#249) * Refactor channel terminology from presence to session * Add backend-disable-webhook-validation to CI

Commit:594f0de
Author:longhoang-lewy
Committer:GitHub

Apply updates from yorkie-js-sdk 0.6.43 (#247) * Apply updates from yorkie-js-sdk 0.6.43 * Refactor Channel example room management with categories * Bump yorkieVersion to 0.6.43

Commit:6cca038
Author:longhoang-lewy
Committer:GitHub

Apply updates from yorkie-js-sdk 0.6.42 (#246)

Commit:fcd6ab5
Author:longhoang-lewy
Committer:GitHub

Apply updates from yorkie-js-sdk 0.6.41 (#244) * feat: add GetRevision and align revision RPC fields with js-sdk v0.6.41 - Regenerate proto: add GetRevision RPC + GetRevisionRequest/Response - ListRevisionsRequest project_id -> client_id - RestoreRevisionRequest revision_id field 2 -> document_id, revision_id -> field 4 - Add Client.getRevision and YorkieService wrapper - Note RevisionSummary.snapshot is now YSON format * feat: port YSON module from js-sdk v0.6.41 - YSONValue enum + YSONText/YSONTree value types - YSON.parse (regex preprocess + JSONSerialization + postprocess), textToString, treeToXML - Type guards (isText/isTree/isInt/isLong/isDate/isBinData/isCounter/isObject) - 24 unit tests ported from yson_test.ts * refactor: thread OpSource and ExecutionResult through Operation.execute Foundation for undo/redo (js-sdk parity): - Operation.execute now returns ExecutionResult{opInfos, reverseOp?} and takes source: OpSource - Each operation keeps its body as a private executeOpInfos helper; reverseOp is nil for now - Change.execute threads source; Document.applyChanges passes source Behavior-preserving; reverse-op generation lands in follow-up commits. * feat: add CRDTRoot.findElementPairByCreatedAt for undo/redo ancestor walk Phase 4a CRDT primitive. (getPreviousCreatedAt already exists on CRDTArray/RGATreeList; CRDTObject.set executedAt deferred to the reverse-Set path.) * feat: generate reverse operations for undo/redo Phase 4b (js-sdk parity): - Operation.execute returns ExecutionResult? (nil = skip during undo/redo) - Add->Remove, Remove->Add/Set, Move->Move, Set->Set/Remove, ArraySet->ArraySet - Set/Remove ancestor-removed undo-skip; Set undo deregister of tombstoned element - setCreatedAt/setPrevCreatedAt mutators for history reconciliation - reverse ops carry a placeholder executedAt, reassigned at undo time (Phase 5) Behavior-preserving: reverse paths only run when source == .undoRedo (not yet wired). * feat: add History for undo/redo stacks Phase 4c: History class with undo/redo stacks (max depth 50), push/pop/clear, and reconcileCreatedAt (value-type-safe for struct ops). HistoryOperation enum covers operation + presence cases. reconcileTextEdit deferred to the reverse-Edit phase; presence-undo wiring deferred. * feat: wire undo history into the local update path Phase 5a: - Change.execute returns ChangeExecutionResult{operations, opInfos, reverseOps} - Document.update pushes reverseOps to the undo stack, clears redo on new ops, and reconciles ArraySet createdAt in the history - internalHistory/isUpdating state added executeUndoRedo + public undo/redo facade follow in Phase 5b. * feat: implement Document undo/redo execution Phase 5b (js-sdk parity): - Public canUndo/canRedo + undo()/redo(); executeUndoRedo applies reverse ops on a fresh change (source .undoRedo), reassigning executedAt and createdAt for Add/ArraySet, pushing the inverse onto the opposite stack - isUpdating guard; clearHistory on snapshot apply - CRDTElement.setCreatedAt (createdAt now settable; Primitive/Counter let->var) - 5 local undo/redo unit tests Known limitation (XCTExpectFailure + follow-up): undoing an object-property overwrite needs the ElementRHT positionedAt mechanism (newer than the iOS port). * feat: implement reverse EditOperation for Text undo/redo (#1126) Phase 6 (js-sdk parity): - RGATreeSplit.normalizePos/refinePos; edit returns removedValues - CRDTText refinePos/normalizePos/posToIndex; edit returns removedValues - EditOperation is now a class; computes a reverse edit (re-inserts removed content), refines its range on undo replay, and reconciles its range against remote edits (reconcileOperation, 6 cases) - History.reconcileTextEdit; Document.applyChanges reconciles text-edit ranges - 2 text undo/redo unit tests (insert + delete) pass * test: add undo/redo coverage; chore: bump CI server to v0.6.41; docs: changelog Phase 7: - test-writer: array add/remove/move undo/redo, reconcileOperation range-shift, ancestor-removed undo-skip (unit) + a two-client convergence integration test - critic-reviewer fixes: document deliberate ArraySet nil-reverse divergence and the intentional reconcileTextEdit stack-copy - bump CI yorkie server pin + docker-compose images to v0.6.41 - CHANGELOG v0.6.41 entry * feat(example): add Undo/Redo to RichTextEditor; expose Document.clearHistory - ContentViewModel.undo()/redo() re-render from the document snapshot; refresh canUndo/canRedo on every document event; toolbar Undo/Redo buttons - Make Document.clearHistory public and call it after the one-time content scaffolding in initializeClient, so undo can never remove 'content' and break the editor (fixes 'get(key:) content does not exist' after undo) Both the SDK and the RichTextEditor example build (BUILD SUCCEEDED). * fix(example): defer undo/redo state publish; use local server - refreshUndoRedoState() publishes canUndo/canRedo on the next main-actor turn to avoid 'Publishing changes from within view updates' (it is called from the document subscription, which can fire during a SwiftUI view update) - point the RichTextEditor client at the local server (no API key needed) * chore(examples): apply formatter output and build artifact updates SwiftFormat-on-save reformatting (RichTextEditor + Todomvc), Xcode package resolution churn, and build info plist updates. No functional changes. * fix: preserve document order of removed text nodes in reverse edit Pre-PR critic found a convergence bug: RGATreeSplit.deleteNodes returned removed nodes in a Swift Dictionary (unordered), so undoing a multi-node text deletion restored scrambled content and diverged from peers. Return removed nodes in document order instead. - add multi-node delete-undo test asserting exact restored content - add deterministic EditOperation.reconcileOperation 6-case + no-op tests - lint fixes (redundantSelf/hoistTry in YSON, identifier names, orphaned doc comment) * docs: backfill PR link in v0.6.41 changelog entry

Commit:f5ae52b
Author:longhoang-lewy
Committer:GitHub

Apply updates from yorkie-js-sdk 0.6.40 (#243) * Apply updates from yorkie-js-sdk 0.6.40 Port revision management (yorkie-js-sdk#1127): regenerate protobuf (RevisionSummary field renumber + Project/UpdatableProjectFields auto_revision_enabled), add the RevisionSummary type + converter, and Client.createRevision/listRevisions/restoreRevision with an integration test. * docs: add v0.6.40 changelog entry

Commit:bf5f6f8
Author:longhoang-lewy
Committer:GitHub

Apply updates from yorkie-js-sdk 0.6.39 * Apply updates from yorkie-js-sdk 0.6.39 Port additive protocol changes from yorkie-js-sdk v0.6.39: - resources.proto: add ChannelSummary and RevisionSummary messages - yorkie.proto: add CreateRevision/ListRevisions/RestoreRevision RPCs and their request/response messages - Regenerate Swift protobuf/connect sources Pin buf swift plugins (apple/swift v1.37.0, connectrpc/swift v1.0.3) so protobuf generation is reproducible and stays free of unrelated codegen drift (e.g. the nonisolated keyword in newer protoc-gen-swift). * chore: bump CI yorkie server to v0.6.39 * chore: resolve build warnings across SDK and integration tests - Drop spurious 'await' on APIs that became synchronous in the v0.6.39 sync (1348 call sites across the integration suite; TextViewModel example). - CRDTArray.deepcopy: explicit '_ = try?' for the discarded insert result. - ChangeID.setClocks: remove dead 'var vector' clone (value type, never mutated). - JSONArray.moveAfterByIndex: drop no-op @discardableResult (Void return). - TextIntegrationTests: 'var' -> 'let' for never-mutated iterators. * docs: add CHANGELOG.md reconstructed from GitHub releases Reconstructs the project changelog from all 43 GitHub Releases (v0.2.19 -> v0.6.37) in Keep a Changelog format. * chore: satisfy swiftformat redundantSelf in DocumentIntegrationTests The await removal exposed bare property accesses that the project's '--self insert' rule requires to be qualified. Applied swiftformat 0.55.6 (the CI-pinned version): 14 'self.' insertions, no behaviour change. * docs: add v0.6.39 changelog entry

Commit:b012eff
Author:longhoang-lewy
Committer:GitHub

Apply updates from yorkie-js-sdk 0.6.36 (#240)

Commit:ad6444e
Author:Ivan
Committer:GitHub

Apply updates from yorkie-js-sdk 0.6.34 (#238) * Sync 0.6.34 * Fix swiftlint * Fix swift format * Remove duplicate files * Fix test * Fix test * Fix test * Fix swiftformat * Fix unit test * Remove unused code

Commit:3b02ee3
Author:Ivan
Committer:GitHub

Apply updates from yorkie-js-sdk 0.6.28 (#236) * Sync 0.6.28 * Update brew 0.6.28

Commit:66bd225
Author:Ivan
Committer:GitHub

Apply updates from yorkie-js-sdk 0.6.23 (#223) * Sync #1030 * Sync #1037 * Import Helpers * Fix review

Commit:efc2437
Author:ivan.do

Sync #1038

Commit:344bd57
Author:Ivan
Committer:GitHub

Apply updates from yorkie-js-sdk 0.6.18 (#219) * Sync #1010 * Update Yorkie server 0.6.18 * Resolve dependency * Specific protobuf to 1.31 * Require swift proto buf as 1.31 * Fix swiftformat * Add Yorkie Test Helper * Fix swift review Remove unused file

Commit:8f9d9bb
Author:Ivan
Committer:GitHub

Apply updates from yorkie-js-sdk 0.6.13 (#215) * Sync #992 * Sync https://github.com/yorkie-team/yorkie-js-sdk/pull/993 * Fix brew install from local file * Fix swiftlint * fix swiftformat * Fix swiftformat * Fix swiftformat * Remove fatalError * Update Sources/Document/Document.swift Co-authored-by: James (An Nguyen) <nguyenngocan.hcmut@gmail.com> * Fix wrong definition of Function --------- Co-authored-by: James (An Nguyen) <nguyenngocan.hcmut@gmail.com>

Commit:9e50901
Author:Ivan
Committer:GitHub

Apply updates from yorkie-js-sdk 0.6.10 (#213) * TS tree_style_operation.ts packages/sdk/src/document/operation * Sync #969 * Configure yorkie server 0.6.10 * Fix swiftlint * Fix swiftFormat * Fix swiftformat * Fix swiftFormat

Commit:9712ad6
Author:Ivan
Committer:GitHub

Apply updates from yorkie-js-sdk 0.6.0 (#206) * Sync #938 * Sync #942 * fix swiftlint * Remove unused code * Update yorkie.pb

Commit:964729c
Author:LOG
Committer:GitHub

Apply updates from yorkie-js-sdk v0.5.3 (#199) * Add (Pb)VersionVector proto message and matching VersionVector struct * Update converter to use version vector * Update Document to handle version vector in applychangepack * Define version vector and add into change_id, changepack * Update GC in CRDT Root to use min synced version vector * Add YorkieTestHelper target for shared test helpers * Apply missing version vector updates * Apply version vector to tests * Fix: Import YorkieTestHelper only during `swift test` to resolve test command failure

Commit:748134e
Author:LOG
Committer:GitHub

Apply updates from yorkie-js-sdk v0.5.2 (#197) * Add support for a new operation type ArraySet in the protocol * Bump minimum deployment target to iOS 15 * Update dependency versions * Apply review comments by coderabbitai

Commit:1aa3dd8
Author:LOG
Committer:GitHub

Apply updates from yorkie-js-sdk v0.4.27 (#192) * Added the generated ErrorInfo model from googleapis/google/rpc/error_details.proto * Add YorkieService wrapper for YorkieServiceClient with API mocking support * Handle retry for syncLoop and watchLoop / Handle ErrClientNotActivated and ErrClientNotFound * Fix watch loop reconnect timer issue in manual syncMode * Fix ensure onlineClients are reset on document detach * Handle local changes correctly when receiving snapshot * Fix Xcode warnings * Apply SwiftFormat * Fix 1 failing test in ClientIntegrationTests * Apply review comments by coderabbitai

Commit:01142d8
Author:Jung gyun Ahn
Committer:GitHub

Add RHTNode removal to converter for consistency (#182) * Fix incorrect indexes in TreeChange * Update swift-integration.yml * Update swift-integration.yml * Update swift-integration.yml * Add RHTNode removal to converter for consistency

Commit:4b063d8
Author:Jung gyun Ahn
Committer:GitHub

Handle concurrent editing and styling in Tree (#166)

Commit:cbde6a0
Author:Jung gyun Ahn
Committer:GitHub

Implement Tree.RemoveStyle (#158) * Implement Protocol Changes for Tree.RemoveStyle * merge similar functions

Commit:314b944
Author:Jung gyun Ahn
Committer:GitHub

Restore interface changes due to server DB sharding (#148) * Revert "Reflect interface changes of server DB sharding" This reverts commit a56bb269a7039f3d8e15423269bc7a5ae40e1b9f. * update ci docker version

Commit:b1d58ea
Author:Jung gyun Ahn
Committer:GitHub

Reflect interface changes of server DB sharding (#145) * Reflect interface changes of server DB sharding * fix lint

Commit:6dacc0d
Author:Jung gyun Ahn
Committer:GitHub

Implement splitLevel of Tree.Edit (#139) * Refactor Tree codes * correct lint error * Implement splitLevel of Tree.Edit

Commit:f15db57
Author:Jung gyun Ahn
Committer:GitHub

Support concurrent formatting of Text (#116) * refactor TCs of Text * Support concurrent formatting of Text

Commit:2310f0a
Author:Jung gyun Ahn
Committer:GitHub

Change TreeNode to have IDs instead of insPrev, insNext (#103) * Change the value of XXXChange to Change in Document.subscribe * initial commit * add Test codes. * Expose pathToIndex API * Bump up protobuf * change yorkie server to 0.4.5 * Remove duplicated codes * remove redundant codes * Initial commit * Correcting misported code * Add TC for presence * change xcode version to 14.3.1 * Update Tree.edit to allow insertion of multiple contents at once * Replace selection with presence * Rename TextRangeStruct to TextPosStructRange * Clean up methods related to presence * Add presence.get() to get presence value in doc.update() * Change 'Documents' from plural to singular in DocEvent * Cleanup proto * Concurrent case handling for Yorkie.tree * fix tree style TC * Change TreeNode to have IDs instead of insPrev, insNext * correct typo. and remove forced unwrap * apply review comment * apply review comment * correct typo

Commit:f200e12
Author:Jung gyun Ahn
Committer:GitHub

Concurrent case handling for Yorkie.tree (#102) * Change the value of XXXChange to Change in Document.subscribe * initial commit * add Test codes. * Expose pathToIndex API * Bump up protobuf * change yorkie server to 0.4.5 * Remove duplicated codes * remove redundant codes * Initial commit * Correcting misported code * Add TC for presence * change xcode version to 14.3.1 * Update Tree.edit to allow insertion of multiple contents at once * Replace selection with presence * Rename TextRangeStruct to TextPosStructRange * Clean up methods related to presence * Add presence.get() to get presence value in doc.update() * Change 'Documents' from plural to singular in DocEvent * Cleanup proto * Concurrent case handling for Yorkie.tree * fix tree style TC * correct typo. and remove forced unwrap * apply review comment * apply review comment * correct typo

Commit:87fd0f5
Author:Jung gyun Ahn
Committer:GitHub

Cleanup proto (#101) * Change the value of XXXChange to Change in Document.subscribe * initial commit * add Test codes. * Expose pathToIndex API * Bump up protobuf * change yorkie server to 0.4.5 * Remove duplicated codes * remove redundant codes * Initial commit * Correcting misported code * Add TC for presence * change xcode version to 14.3.1 * Update Tree.edit to allow insertion of multiple contents at once * Replace selection with presence * Rename TextRangeStruct to TextPosStructRange * Clean up methods related to presence * Add presence.get() to get presence value in doc.update() * Change 'Documents' from plural to singular in DocEvent * Cleanup proto * apply review comment * apply review comment * correct typo

Commit:8be3818
Author:Jung gyun Ahn
Committer:GitHub

Change 'Documents' from plural to singular in DocEvent (#100) * Change the value of XXXChange to Change in Document.subscribe * initial commit * add Test codes. * Expose pathToIndex API * Bump up protobuf * change yorkie server to 0.4.5 * Remove duplicated codes * remove redundant codes * Initial commit * Correcting misported code * Add TC for presence * change xcode version to 14.3.1 * Update Tree.edit to allow insertion of multiple contents at once * Replace selection with presence * Rename TextRangeStruct to TextPosStructRange * Clean up methods related to presence * Add presence.get() to get presence value in doc.update() * Change 'Documents' from plural to singular in DocEvent * apply review comment * apply review comment * correct typo

Commit:35b77a1
Author:Jung gyun Ahn
Committer:GitHub

Move Presence from Client to Document (#94) * Change the value of XXXChange to Change in Document.subscribe * initial commit * add Test codes. * Expose pathToIndex API * Bump up protobuf * change yorkie server to 0.4.5 * Remove duplicated codes * remove redundant codes * Initial commit * Correcting misported code * Add TC for presence * change xcode version to 14.3.1

Commit:580bcd9
Author:Jung gyun Ahn
Committer:GitHub

Allow multi tree nodes when edit (#93) * Change the value of XXXChange to Change in Document.subscribe * initial commit * add Test codes. * Expose pathToIndex API * Bump up protobuf * change yorkie server to 0.4.5 * Remove duplicated codes * remove redundant codes

Commit:653c066
Author:Jung gyun Ahn
Committer:GitHub

Implement yorkie.Tree for text editors using tree model (#90) * Change the value of XXXChange to Change in Document.subscribe * initial commit * add Test codes. * Cleanup of test-related terminology * Expose pathToIndex API * Bump up protobuf * modify action script * correct script * disable todo lint rule * Revert "Expose pathToIndex API" This reverts commit d5a6a0226601ef3dc008939e1c030768c5b9bd0c. * Revert "Bump up protobuf " This reverts commit 89f1ba1360ae9784919647fe113d92264c1d20c1. * set yorkie server version to 0.4.4 * set yorkie server to 0.4.4 * fix sync() concurrency * bump to 0.4.4 * Update swift-integration.yml * Revert "Update swift-integration.yml" This reverts commit 318dc04b03ca6acde97901efca4dcbae9eab1c15. * add more debug info. * Update swift-integration.yml * fix typo * change params to underscore * fix compile error * Revert "Update swift-integration.yml" This reverts commit 318dc04b03ca6acde97901efca4dcbae9eab1c15. * fix misused idString * change the lamport in the TimeTicketStruct to string * cleanup codes * complete == function * change throw exception to assert * remove unnecessary code * change throw exception to precondition * open ElementNode init() * rename JSON nodes * revert edit() parameter to 0.4.4 * remove forced unwrap * fix lint error * fix JSONTree iterration * fix tc validation function * change docker action * Revert "change docker action " This reverts commit f5120da2a6ebfedee179fde3c0a9970917d14d09. * Change JSONTree attributes value type to Any * remove forced unwrap * fix bool converting * Revert "fix bool converting" This reverts commit b761b93ef7c9374654b137600ee0fb7c36e144a9. * remove duplicated codes * Update swift-integration.yml * cleanup ci action * Update swift-integration.yml * Update swift-integration.yml * Fix compile error

Commit:8e84992
Author:Jung gyun Ahn
Committer:GitHub

Add Client sync mode (#65) * Add Client sync mode * change syncMode to realtimeSyncMode * Add test codes * fix warning by xcode 14.3 * revert codes for xcode 14.3 * Remove unnecessary checkpoint validation from applyChangePack * Match function names with JS * lint code * Remove pullOnly mode * revert a unnessary change * Revert codes position to diff changes of codes clearly * Revert codes * Add comments for SyncMode * Refactor the codes related to getPeerPresence (#77) * add test code for pauseRemoteChanges and resumeRemoteChanges * remove waits in TC * revert waits in TC

Commit:b5ec0be
Author:Jung gyun Ahn
Committer:GitHub

Add RemoveDocument API (#64) * Add RemoveDocument API * Elaborate on the value of the PeersChanged event * Add TC for the stream connection status change event * Fix the watchLoop start and stop for pause, resume * Removed the codes for changing the status of the doc, before sending the API * Invalidate watchLoop reconnection timer when disconnecting stream * Throws an error when server returns removed doc for the attached result

Commit:4387216
Author:Jung gyun Ahn
Committer:GitHub

Remove duplicated key from TextNodeAttr (#52)

Commit:c865d0a
Author:Jung gyun Ahn
Committer:GitHub

Implement Text Element (#50) * Implements RGATreeSplit * Add throw noSuchElement error for failed forced unwrap

Commit:e48397b
Author:원형식
Committer:GitHub

Initial project setup (#2) Project structure based on a default swift package project and yorkie-js-sdk. - grpc-swift - swift-protobuf - SwiftLint - SwiftFormat