craft-native 0.0.435 → 0.0.436
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/templates/CraftNativeMutation.swift +18 -3
- package/dist/ios/templates/testing/DriverTests.swift.template +24 -4
- package/dist/macos/src/index.js +1 -1
- package/dist/macos/src/toolchain.d.ts +1 -1
- package/dist/macos/templates/CraftMacRenderer.swift +74 -20
- package/dist/macos/templates/CraftMacRuntime.swift +34 -2
- package/dist/macos/templates/CraftNativeMutation.swift +18 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3047,7 +3047,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
|
|
|
3047
3047
|
`);
|
|
3048
3048
|
}
|
|
3049
3049
|
// package.json
|
|
3050
|
-
var version = "0.0.
|
|
3050
|
+
var version = "0.0.436";
|
|
3051
3051
|
|
|
3052
3052
|
// bin/cli.ts
|
|
3053
3053
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|
package/dist/index.cjs
CHANGED
package/dist/index.js
CHANGED
|
@@ -18,10 +18,22 @@ struct CraftNativeMutationFailure: Error, Equatable {
|
|
|
18
18
|
struct CraftNativeMutationResult {
|
|
19
19
|
let batchId: String
|
|
20
20
|
let revision: Int
|
|
21
|
-
let document: [String: Any]?
|
|
22
21
|
let updatedNodeIds: [String]
|
|
23
22
|
let affectedNodeIds: [String]
|
|
24
23
|
let requiresFullRender: Bool
|
|
24
|
+
private let materialize: () -> [String: Any]?
|
|
25
|
+
/// The committed tree, built when asked for: a host that redraws only the
|
|
26
|
+
/// nodes a batch touched never needs the whole tree.
|
|
27
|
+
var document: [String: Any]? { materialize() }
|
|
28
|
+
|
|
29
|
+
init(batchId: String, revision: Int, document: @escaping () -> [String: Any]?, updatedNodeIds: [String], affectedNodeIds: [String], requiresFullRender: Bool) {
|
|
30
|
+
self.batchId = batchId
|
|
31
|
+
self.revision = revision
|
|
32
|
+
self.materialize = document
|
|
33
|
+
self.updatedNodeIds = updatedNodeIds
|
|
34
|
+
self.affectedNodeIds = affectedNodeIds
|
|
35
|
+
self.requiresFullRender = requiresFullRender
|
|
36
|
+
}
|
|
25
37
|
}
|
|
26
38
|
|
|
27
39
|
/// Validates and applies versioned native-tree mutations without touching UIKit.
|
|
@@ -91,6 +103,8 @@ final class CraftNativeMutationDocument {
|
|
|
91
103
|
var updatedNodeIds: [String] = []
|
|
92
104
|
var affectedNodeIds: [String] = []
|
|
93
105
|
var requiresFullRender = false
|
|
106
|
+
// Only an operation that adds, moves or removes nodes can leave one unreachable.
|
|
107
|
+
var reshaped = false
|
|
94
108
|
for (index, value) in operations.enumerated() {
|
|
95
109
|
guard let operation = value as? [String: Any], let name = operation["op"] as? String else {
|
|
96
110
|
throw CraftNativeMutationFailure(
|
|
@@ -99,6 +113,7 @@ final class CraftNativeMutationDocument {
|
|
|
99
113
|
operationIndex: index
|
|
100
114
|
)
|
|
101
115
|
}
|
|
116
|
+
if name != "updateNode" { reshaped = true }
|
|
102
117
|
do {
|
|
103
118
|
switch name {
|
|
104
119
|
case "createNode":
|
|
@@ -147,7 +162,7 @@ final class CraftNativeMutationDocument {
|
|
|
147
162
|
guard let root = candidateNodes[candidateRoot], root.parent == nil else {
|
|
148
163
|
throw CraftNativeMutationFailure("INVALID_TREE", "root node must not have a parent")
|
|
149
164
|
}
|
|
150
|
-
guard descendants(of: candidateRoot, nodes: candidateNodes).count + 1 == candidateNodes.count else {
|
|
165
|
+
guard !reshaped || descendants(of: candidateRoot, nodes: candidateNodes).count + 1 == candidateNodes.count else {
|
|
151
166
|
throw CraftNativeMutationFailure("INVALID_TREE", "all nodes must be reachable from the root")
|
|
152
167
|
}
|
|
153
168
|
}
|
|
@@ -158,7 +173,7 @@ final class CraftNativeMutationDocument {
|
|
|
158
173
|
return CraftNativeMutationResult(
|
|
159
174
|
batchId: batchId,
|
|
160
175
|
revision: nextRevision,
|
|
161
|
-
document: candidateRoot.flatMap { materialize($0, nodes: candidateNodes) },
|
|
176
|
+
document: { [candidateRoot, candidateNodes, weak self] in candidateRoot.flatMap { self?.materialize($0, nodes: candidateNodes) } },
|
|
162
177
|
updatedNodeIds: updatedNodeIds,
|
|
163
178
|
affectedNodeIds: affectedNodeIds,
|
|
164
179
|
requiresFullRender: requiresFullRender
|
|
@@ -285,8 +285,15 @@ final class DriverTests: XCTestCase {
|
|
|
285
285
|
return (parts[0], parts.count > 1 ? Double(parts[1]) ?? 10 : 10)
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
-
/// A
|
|
289
|
-
///
|
|
288
|
+
/// A switch reports "0" or "1". Any other value is not a switch state to
|
|
289
|
+
/// wait on, so the tap stands as it would for any other control.
|
|
290
|
+
func switchFlipped(from before: String?, to after: String?) -> Bool {
|
|
291
|
+
let states: Set<String> = ["0", "1"]
|
|
292
|
+
guard let before, let after, states.contains(before), states.contains(after) else { return true }
|
|
293
|
+
return before != after
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/// Deliver one gesture; the command's observation determines success.
|
|
290
297
|
func press(_ element: XCUIElement) {
|
|
291
298
|
if element.isHittable { element.tap() }
|
|
292
299
|
else { element.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap() }
|
|
@@ -438,8 +445,21 @@ final class DriverTests: XCTestCase {
|
|
|
438
445
|
else { XCTFail("Element not found"); note("FAIL tapexact \(arg): NOT FOUND") }
|
|
439
446
|
case "tapid":
|
|
440
447
|
let target = identified(arg)
|
|
441
|
-
if target.waitForExistence(timeout: 10) {
|
|
442
|
-
else
|
|
448
|
+
if !target.waitForExistence(timeout: 10) { XCTFail("Element not found"); note("FAIL tapid \(arg): NOT FOUND") }
|
|
449
|
+
else if target.elementType == .switch {
|
|
450
|
+
// Observe one physical gesture. Retapping could conceal a
|
|
451
|
+
// renderer or hit-testing failure (HQ agreement job 7570).
|
|
452
|
+
let before = target.value as? String
|
|
453
|
+
press(target)
|
|
454
|
+
let settle = Date().addingTimeInterval(1.5)
|
|
455
|
+
var flipped = false
|
|
456
|
+
repeat {
|
|
457
|
+
flipped = switchFlipped(from: before, to: target.value as? String)
|
|
458
|
+
if !flipped { Thread.sleep(forTimeInterval: 0.25) }
|
|
459
|
+
} while !flipped && Date() < settle
|
|
460
|
+
check(flipped, "tapid \(arg) flips the switch")
|
|
461
|
+
}
|
|
462
|
+
else { press(target); note("tapid \(arg): ok") }
|
|
443
463
|
case "longpressid":
|
|
444
464
|
let target = identified(arg)
|
|
445
465
|
guard target.waitForExistence(timeout: 10), target.isHittable else {
|
package/dist/macos/src/index.js
CHANGED
|
@@ -43,7 +43,7 @@ import process from "process";
|
|
|
43
43
|
var macosToolchain = {
|
|
44
44
|
zig: "0.18.0-dev.252+86dc5ce25",
|
|
45
45
|
sources: {
|
|
46
|
-
"zig-js": { repository: "zig-utils/zig-js", revision: "
|
|
46
|
+
"zig-js": { repository: "zig-utils/zig-js", revision: "44a0a2ba814a911ac701c44073bd8d7d8ff6a22d", paths: ["src", "include", "tests", "tools", "docs", "bench", "conformance"] },
|
|
47
47
|
"zig-regex": { repository: "zig-utils/zig-regex", revision: "fdd9c2e333ea6ac2fd06d080d939ef7f11d61b1b", paths: ["src"] },
|
|
48
48
|
"zig-gc": { repository: "zig-utils/zig-gc", revision: "73c802e05768a729614e9c4296abb118aa06faf9", paths: ["src"] }
|
|
49
49
|
}
|
|
@@ -9,7 +9,7 @@ export declare const macosToolchain: {
|
|
|
9
9
|
readonly sources: {
|
|
10
10
|
readonly 'zig-js': {
|
|
11
11
|
readonly repository: 'zig-utils/zig-js';
|
|
12
|
-
readonly revision: '
|
|
12
|
+
readonly revision: '44a0a2ba814a911ac701c44073bd8d7d8ff6a22d';
|
|
13
13
|
readonly paths: readonly ['src', 'include', 'tests', 'tools', 'docs', 'bench', 'conformance'];
|
|
14
14
|
};
|
|
15
15
|
readonly 'zig-regex': {
|
|
@@ -288,6 +288,8 @@ final class CraftMacBox: NSView, NSTextFieldDelegate, NSTextViewDelegate, NSDrag
|
|
|
288
288
|
static var measuredCount = 0
|
|
289
289
|
private struct MeasureKey: Hashable { let width: CGFloat, stretch: Bool }
|
|
290
290
|
private var measured: [MeasureKey: CGSize] = [:]
|
|
291
|
+
/// What this box measured is stale: it or something inside it changed (CraftMacRenderer.update).
|
|
292
|
+
func forgetMeasurements() { measured.removeAll(keepingCapacity: true) }
|
|
291
293
|
private var measuredGeneration = -1
|
|
292
294
|
var componentRoot: CraftMacBox? { props["data-stx-component-wrapper"] != nil && flow?.boxes.count == 1 && flow?.boxes.first?.kind != "Modal" ? flow?.boxes.first : nil }
|
|
293
295
|
var layoutStyle: [String: Any] {
|
|
@@ -966,6 +968,77 @@ final class CraftMacRenderer {
|
|
|
966
968
|
weak var window: NSWindow?
|
|
967
969
|
var sheets: [String: NSPanel] = [:]
|
|
968
970
|
init(runtime: CraftMacRuntime) { self.runtime = runtime }
|
|
971
|
+
/// The box for `node`, made or reconfigured, and its children's in order; every id drawn goes into `retained`.
|
|
972
|
+
func reconcile(_ node: [String: Any], retained: inout Set<String>) -> CraftMacBox {
|
|
973
|
+
let id = node["id"] as? String ?? "root", kind = node["type"] as? String ?? "View"
|
|
974
|
+
retained.insert(id)
|
|
975
|
+
let box = boxes[id] ?? CraftMacBox(id: id, kind: kind, renderer: self)
|
|
976
|
+
boxes[id] = box; box.configure(node)
|
|
977
|
+
if let flow = box.flow, kind != "Button" {
|
|
978
|
+
let children = (node["children"] as? [Any] ?? []).compactMap { $0 as? [String: Any] }.map { reconcile($0, retained: &retained) }
|
|
979
|
+
for child in flow.boxes where !children.contains(where: { $0 === child }) { child.removeFromSuperview() }
|
|
980
|
+
flow.boxes = children
|
|
981
|
+
for child in children where child.superview !== flow && sheets[child.id] == nil { flow.addSubview(child) }
|
|
982
|
+
// `zIndex` draws a child above its siblings (an overlay over a web page), highest last.
|
|
983
|
+
let raised = children.filter { (number($0.layoutStyle["zIndex"]) ?? 0) > 0 && $0.superview === flow }
|
|
984
|
+
.sorted { (number($0.layoutStyle["zIndex"]) ?? 0) < (number($1.layoutStyle["zIndex"]) ?? 0) }
|
|
985
|
+
if !raised.isEmpty, Array(flow.subviews.suffix(raised.count)) != raised {
|
|
986
|
+
for child in raised { flow.addSubview(child, positioned: .above, relativeTo: nil) }
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
return box
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/// A batch drawn where it happened rather than across the window: each changed node's subtree
|
|
993
|
+
/// (or, for a node the batch created, its nearest drawn ancestor's) is reconciled again, and only
|
|
994
|
+
/// those boxes and the boxes above them measure and lay out again. A hover on one row used to
|
|
995
|
+
/// reconfigure every view in the window and forget every measurement. False when it cannot be
|
|
996
|
+
/// done in place (nothing drawn yet, a sheet, a node changing kind): the caller renders the tree.
|
|
997
|
+
func update(_ ids: [String], parentOf: (String) -> String?, node nodeFor: (String) -> [String: Any]?) -> Bool {
|
|
998
|
+
guard !boxes.isEmpty else { return false }
|
|
999
|
+
var targets: [String] = []
|
|
1000
|
+
for id in ids {
|
|
1001
|
+
var current: String? = id
|
|
1002
|
+
while let candidate = current, boxes[candidate] == nil { current = parentOf(candidate) }
|
|
1003
|
+
guard let target = current else { return false }
|
|
1004
|
+
if !targets.contains(target) { targets.append(target) }
|
|
1005
|
+
}
|
|
1006
|
+
// A target inside another is drawn with it.
|
|
1007
|
+
let chosen = targets.filter { target in
|
|
1008
|
+
guard let box = boxes[target] else { return false }
|
|
1009
|
+
return !targets.contains { other in other != target && boxes[other].map { box.isDescendant(of: $0) } == true }
|
|
1010
|
+
}
|
|
1011
|
+
CATransaction.begin(); CATransaction.setDisableActions(true)
|
|
1012
|
+
defer { CATransaction.commit() }
|
|
1013
|
+
for target in chosen {
|
|
1014
|
+
guard let box = boxes[target], box.window === window, sheets[target] == nil, box.kind != "Modal",
|
|
1015
|
+
let node = nodeFor(target), (node["type"] as? String ?? "View") == box.kind else { return false }
|
|
1016
|
+
var before = Set<String>()
|
|
1017
|
+
collectIds(box, into: &before)
|
|
1018
|
+
var retained = Set<String>()
|
|
1019
|
+
_ = reconcile(node, retained: &retained)
|
|
1020
|
+
for id in before where !retained.contains(id) {
|
|
1021
|
+
guard let gone = boxes.removeValue(forKey: id) else { continue }
|
|
1022
|
+
gone.imageTask?.cancel(); gone.removeFromSuperview()
|
|
1023
|
+
}
|
|
1024
|
+
for id in retained { if let drawn = boxes[id] { drawn.forgetMeasurements(); drawn.needsLayout = true } }
|
|
1025
|
+
var above: NSView? = box.superview
|
|
1026
|
+
while let view = above {
|
|
1027
|
+
(view as? CraftMacBox)?.forgetMeasurements()
|
|
1028
|
+
view.needsLayout = true
|
|
1029
|
+
above = view.superview
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
root.needsLayout = true
|
|
1033
|
+
root.layoutSubtreeIfNeeded()
|
|
1034
|
+
return true
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
private func collectIds(_ box: CraftMacBox, into ids: inout Set<String>) {
|
|
1038
|
+
ids.insert(box.id)
|
|
1039
|
+
for child in box.flow?.boxes ?? [] { collectIds(child, into: &ids) }
|
|
1040
|
+
}
|
|
1041
|
+
|
|
969
1042
|
func render(_ document: [String: Any]) {
|
|
970
1043
|
CATransaction.begin(); CATransaction.setDisableActions(true)
|
|
971
1044
|
defer { CATransaction.commit() }
|
|
@@ -976,26 +1049,7 @@ final class CraftMacRenderer {
|
|
|
976
1049
|
let bottom = bounds.maxY >= (scroll.documentView?.bounds.height ?? 0) - 8
|
|
977
1050
|
scrolls.append((box, bounds.origin, bottom))
|
|
978
1051
|
} }
|
|
979
|
-
|
|
980
|
-
let id = node["id"] as? String ?? "root", kind = node["type"] as? String ?? "View"
|
|
981
|
-
retained.insert(id)
|
|
982
|
-
let box = boxes[id] ?? CraftMacBox(id: id, kind: kind, renderer: self)
|
|
983
|
-
boxes[id] = box; box.configure(node)
|
|
984
|
-
if let flow = box.flow, kind != "Button" {
|
|
985
|
-
let children = (node["children"] as? [Any] ?? []).compactMap { $0 as? [String: Any] }.map(reconcile)
|
|
986
|
-
for child in flow.boxes where !children.contains(where: { $0 === child }) { child.removeFromSuperview() }
|
|
987
|
-
flow.boxes = children
|
|
988
|
-
for child in children where child.superview !== flow && sheets[child.id] == nil { flow.addSubview(child) }
|
|
989
|
-
// `zIndex` draws a child above its siblings (an overlay over a web page), highest last.
|
|
990
|
-
let raised = children.filter { (number($0.layoutStyle["zIndex"]) ?? 0) > 0 && $0.superview === flow }
|
|
991
|
-
.sorted { (number($0.layoutStyle["zIndex"]) ?? 0) < (number($1.layoutStyle["zIndex"]) ?? 0) }
|
|
992
|
-
if !raised.isEmpty, Array(flow.subviews.suffix(raised.count)) != raised {
|
|
993
|
-
for child in raised { flow.addSubview(child, positioned: .above, relativeTo: nil) }
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
return box
|
|
997
|
-
}
|
|
998
|
-
let box = reconcile(document)
|
|
1052
|
+
let box = reconcile(document, retained: &retained)
|
|
999
1053
|
// Boxes were reconfigured and children replaced: nothing measured before now holds.
|
|
1000
1054
|
CraftMacBox.measureGeneration &+= 1
|
|
1001
1055
|
for (id, box) in boxes where !retained.contains(id) {
|
|
@@ -173,7 +173,37 @@ final class CraftMacRuntime {
|
|
|
173
173
|
""")
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
func load(_ bundle: String) {
|
|
176
|
+
func load(_ bundle: String) {
|
|
177
|
+
startProfileIfAsked()
|
|
178
|
+
context.evaluateScript(bundle); ZJSContextRunMicrotasks(context)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/// `CRAFT_JS_PROFILE=<file>`: sample this screen's JavaScript from the moment its bundle loads,
|
|
182
|
+
/// for `CRAFT_JS_PROFILE_MS` milliseconds (5000, every `CRAFT_JS_PROFILE_INTERVAL_US`, 1000),
|
|
183
|
+
/// and write it as Chrome's `.cpuprofile` (DevTools and speedscope open it). A window other
|
|
184
|
+
/// than Main writes beside it, its route in the name.
|
|
185
|
+
private func startProfileIfAsked() {
|
|
186
|
+
let env = ProcessInfo.processInfo.environment
|
|
187
|
+
guard let path = env["CRAFT_JS_PROFILE"], !path.isEmpty, let ref = context.jsGlobalContextRef else { return }
|
|
188
|
+
guard ZJSContextStartCPUProfile(ref, UInt32(env["CRAFT_JS_PROFILE_INTERVAL_US"] ?? "") ?? 0) else { return }
|
|
189
|
+
let seconds = (Double(env["CRAFT_JS_PROFILE_MS"] ?? "") ?? 5000) / 1000
|
|
190
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + seconds) { [weak self] in self?.writeProfile(to: path) }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private func writeProfile(to path: String) {
|
|
194
|
+
guard !disposed, let ref = context.jsGlobalContextRef, let json = ZJSContextStopCPUProfile(ref) else { return }
|
|
195
|
+
defer { JSStringRelease(json) }
|
|
196
|
+
let size = JSStringGetMaximumUTF8CStringSize(json)
|
|
197
|
+
var bytes = [CChar](repeating: 0, count: size)
|
|
198
|
+
JSStringGetUTF8CString(json, &bytes, size)
|
|
199
|
+
let url = URL(fileURLWithPath: path)
|
|
200
|
+
let target = route == "Main" ? url : url.deletingPathExtension().appendingPathExtension("\(route).cpuprofile")
|
|
201
|
+
do {
|
|
202
|
+
try String(cString: bytes).write(to: target, atomically: true, encoding: .utf8)
|
|
203
|
+
fputs("[craft] JavaScript profile written to \(target.path)\n", stderr)
|
|
204
|
+
}
|
|
205
|
+
catch { fputs("[craft] could not write the JavaScript profile to \(target.path): \(error)\n", stderr) }
|
|
206
|
+
}
|
|
177
207
|
|
|
178
208
|
func dispose() {
|
|
179
209
|
guard !disposed else { return }
|
|
@@ -215,7 +245,9 @@ final class CraftMacRuntime {
|
|
|
215
245
|
case "MUTATE":
|
|
216
246
|
do {
|
|
217
247
|
let result = try document.apply(payload)
|
|
218
|
-
|
|
248
|
+
// Drawn where it changed; the whole tree only when the batch reshaped it.
|
|
249
|
+
if result.requiresFullRender || scene?.renderer.update(result.affectedNodeIds, parentOf: document.parentId(of:), node: document.node) != true,
|
|
250
|
+
let tree = result.document { scene?.renderer.render(tree) }
|
|
219
251
|
send("MUTATION_ACK", ["version": 1, "batchId": result.batchId, "revision": result.revision])
|
|
220
252
|
} catch let error as CraftNativeMutationFailure {
|
|
221
253
|
send("MUTATION_ERROR", ["version": 1, "batchId": payload["batchId"] ?? "", "code": error.code, "message": error.message])
|
|
@@ -18,10 +18,22 @@ struct CraftNativeMutationFailure: Error, Equatable {
|
|
|
18
18
|
struct CraftNativeMutationResult {
|
|
19
19
|
let batchId: String
|
|
20
20
|
let revision: Int
|
|
21
|
-
let document: [String: Any]?
|
|
22
21
|
let updatedNodeIds: [String]
|
|
23
22
|
let affectedNodeIds: [String]
|
|
24
23
|
let requiresFullRender: Bool
|
|
24
|
+
private let materialize: () -> [String: Any]?
|
|
25
|
+
/// The committed tree, built when asked for: a host that redraws only the
|
|
26
|
+
/// nodes a batch touched never needs the whole tree.
|
|
27
|
+
var document: [String: Any]? { materialize() }
|
|
28
|
+
|
|
29
|
+
init(batchId: String, revision: Int, document: @escaping () -> [String: Any]?, updatedNodeIds: [String], affectedNodeIds: [String], requiresFullRender: Bool) {
|
|
30
|
+
self.batchId = batchId
|
|
31
|
+
self.revision = revision
|
|
32
|
+
self.materialize = document
|
|
33
|
+
self.updatedNodeIds = updatedNodeIds
|
|
34
|
+
self.affectedNodeIds = affectedNodeIds
|
|
35
|
+
self.requiresFullRender = requiresFullRender
|
|
36
|
+
}
|
|
25
37
|
}
|
|
26
38
|
|
|
27
39
|
/// Validates and applies versioned native-tree mutations without touching UIKit.
|
|
@@ -91,6 +103,8 @@ final class CraftNativeMutationDocument {
|
|
|
91
103
|
var updatedNodeIds: [String] = []
|
|
92
104
|
var affectedNodeIds: [String] = []
|
|
93
105
|
var requiresFullRender = false
|
|
106
|
+
// Only an operation that adds, moves or removes nodes can leave one unreachable.
|
|
107
|
+
var reshaped = false
|
|
94
108
|
for (index, value) in operations.enumerated() {
|
|
95
109
|
guard let operation = value as? [String: Any], let name = operation["op"] as? String else {
|
|
96
110
|
throw CraftNativeMutationFailure(
|
|
@@ -99,6 +113,7 @@ final class CraftNativeMutationDocument {
|
|
|
99
113
|
operationIndex: index
|
|
100
114
|
)
|
|
101
115
|
}
|
|
116
|
+
if name != "updateNode" { reshaped = true }
|
|
102
117
|
do {
|
|
103
118
|
switch name {
|
|
104
119
|
case "createNode":
|
|
@@ -147,7 +162,7 @@ final class CraftNativeMutationDocument {
|
|
|
147
162
|
guard let root = candidateNodes[candidateRoot], root.parent == nil else {
|
|
148
163
|
throw CraftNativeMutationFailure("INVALID_TREE", "root node must not have a parent")
|
|
149
164
|
}
|
|
150
|
-
guard descendants(of: candidateRoot, nodes: candidateNodes).count + 1 == candidateNodes.count else {
|
|
165
|
+
guard !reshaped || descendants(of: candidateRoot, nodes: candidateNodes).count + 1 == candidateNodes.count else {
|
|
151
166
|
throw CraftNativeMutationFailure("INVALID_TREE", "all nodes must be reachable from the root")
|
|
152
167
|
}
|
|
153
168
|
}
|
|
@@ -158,7 +173,7 @@ final class CraftNativeMutationDocument {
|
|
|
158
173
|
return CraftNativeMutationResult(
|
|
159
174
|
batchId: batchId,
|
|
160
175
|
revision: nextRevision,
|
|
161
|
-
document: candidateRoot.flatMap { materialize($0, nodes: candidateNodes) },
|
|
176
|
+
document: { [candidateRoot, candidateNodes, weak self] in candidateRoot.flatMap { self?.materialize($0, nodes: candidateNodes) } },
|
|
162
177
|
updatedNodeIds: updatedNodeIds,
|
|
163
178
|
affectedNodeIds: affectedNodeIds,
|
|
164
179
|
requiresFullRender: requiresFullRender
|