diff --git a/bindings/ios/ZxDocumentRuntime.xcframework/Info.plist b/bindings/ios/ZxDocumentRuntime.xcframework/Info.plist
index 612f37c..4526e4c 100644
--- a/bindings/ios/ZxDocumentRuntime.xcframework/Info.plist
+++ b/bindings/ios/ZxDocumentRuntime.xcframework/Info.plist
@@ -8,12 +8,13 @@
BinaryPath
libzx_document_ffi.a
LibraryIdentifier
- ios-arm64-simulator
+ ios-arm64_x86_64-simulator
LibraryPath
libzx_document_ffi.a
SupportedArchitectures
arm64
+ x86_64
SupportedPlatform
ios
diff --git a/bindings/ios/ZxDocumentRuntime.xcframework/ios-arm64/libzx_document_ffi.a b/bindings/ios/ZxDocumentRuntime.xcframework/ios-arm64/libzx_document_ffi.a
new file mode 100644
index 0000000..e762a9c
Binary files /dev/null and b/bindings/ios/ZxDocumentRuntime.xcframework/ios-arm64/libzx_document_ffi.a differ
diff --git a/bindings/ios/ZxDocumentRuntime.xcframework/ios-arm64_x86_64-simulator/libzx_document_ffi.a b/bindings/ios/ZxDocumentRuntime.xcframework/ios-arm64_x86_64-simulator/libzx_document_ffi.a
new file mode 100644
index 0000000..e1166df
Binary files /dev/null and b/bindings/ios/ZxDocumentRuntime.xcframework/ios-arm64_x86_64-simulator/libzx_document_ffi.a differ
diff --git a/bindings/ios/device/libzx_document_ffi.a b/bindings/ios/device/libzx_document_ffi.a
new file mode 100644
index 0000000..e762a9c
Binary files /dev/null and b/bindings/ios/device/libzx_document_ffi.a differ
diff --git a/bindings/ios/generated/zx_document.swift b/bindings/ios/generated/zx_document.swift
index 33c9d7d..a948d16 100644
--- a/bindings/ios/generated/zx_document.swift
+++ b/bindings/ios/generated/zx_document.swift
@@ -533,7 +533,7 @@ fileprivate struct FfiConverterString: FfiConverter {
if value.data == nil {
return String()
}
- let bytes = UnsafeBufferPointer(start: value.data!.assumingMemoryBound(to: UInt8.self), count: Int(value.len))
+ let bytes = UnsafeBufferPointer(start: value.data!, count: Int(value.len))
return String(bytes: bytes, encoding: String.Encoding.utf8)!
}
@@ -569,11 +569,20 @@ public struct DocumentInfo: Equatable, Hashable {
public var fileSize: UInt64
public var pageCount: UInt32?
public var wordCount: UInt32?
+ public var charCount: UInt32?
+ public var lineCount: UInt32?
+ public var imageWidth: UInt32?
+ public var imageHeight: UInt32?
+ public var imageFormat: String?
+ public var epubChapterCount: UInt32?
+ public var isSearchable: Bool
+ public var supportsPosition: Bool
+ public var supportsAnchor: Bool
public var createdAt: String?
// Default memberwise initializers are never public by default, so we
// declare one manually.
- public init(materialId: String, title: String, materialType: MaterialType, previewMode: PreviewMode, fileSize: UInt64, pageCount: UInt32?, wordCount: UInt32?, createdAt: String?) {
+ public init(materialId: String, title: String, materialType: MaterialType, previewMode: PreviewMode, fileSize: UInt64, pageCount: UInt32?, wordCount: UInt32?, charCount: UInt32?, lineCount: UInt32?, imageWidth: UInt32?, imageHeight: UInt32?, imageFormat: String?, epubChapterCount: UInt32?, isSearchable: Bool, supportsPosition: Bool, supportsAnchor: Bool, createdAt: String?) {
self.materialId = materialId
self.title = title
self.materialType = materialType
@@ -581,6 +590,15 @@ public struct DocumentInfo: Equatable, Hashable {
self.fileSize = fileSize
self.pageCount = pageCount
self.wordCount = wordCount
+ self.charCount = charCount
+ self.lineCount = lineCount
+ self.imageWidth = imageWidth
+ self.imageHeight = imageHeight
+ self.imageFormat = imageFormat
+ self.epubChapterCount = epubChapterCount
+ self.isSearchable = isSearchable
+ self.supportsPosition = supportsPosition
+ self.supportsAnchor = supportsAnchor
self.createdAt = createdAt
}
@@ -607,6 +625,15 @@ public struct FfiConverterTypeDocumentInfo: FfiConverterRustBuffer {
fileSize: FfiConverterUInt64.read(from: &buf),
pageCount: FfiConverterOptionUInt32.read(from: &buf),
wordCount: FfiConverterOptionUInt32.read(from: &buf),
+ charCount: FfiConverterOptionUInt32.read(from: &buf),
+ lineCount: FfiConverterOptionUInt32.read(from: &buf),
+ imageWidth: FfiConverterOptionUInt32.read(from: &buf),
+ imageHeight: FfiConverterOptionUInt32.read(from: &buf),
+ imageFormat: FfiConverterOptionString.read(from: &buf),
+ epubChapterCount: FfiConverterOptionUInt32.read(from: &buf),
+ isSearchable: FfiConverterBool.read(from: &buf),
+ supportsPosition: FfiConverterBool.read(from: &buf),
+ supportsAnchor: FfiConverterBool.read(from: &buf),
createdAt: FfiConverterOptionString.read(from: &buf)
)
}
@@ -619,6 +646,15 @@ public struct FfiConverterTypeDocumentInfo: FfiConverterRustBuffer {
FfiConverterUInt64.write(value.fileSize, into: &buf)
FfiConverterOptionUInt32.write(value.pageCount, into: &buf)
FfiConverterOptionUInt32.write(value.wordCount, into: &buf)
+ FfiConverterOptionUInt32.write(value.charCount, into: &buf)
+ FfiConverterOptionUInt32.write(value.lineCount, into: &buf)
+ FfiConverterOptionUInt32.write(value.imageWidth, into: &buf)
+ FfiConverterOptionUInt32.write(value.imageHeight, into: &buf)
+ FfiConverterOptionString.write(value.imageFormat, into: &buf)
+ FfiConverterOptionUInt32.write(value.epubChapterCount, into: &buf)
+ FfiConverterBool.write(value.isSearchable, into: &buf)
+ FfiConverterBool.write(value.supportsPosition, into: &buf)
+ FfiConverterBool.write(value.supportsAnchor, into: &buf)
FfiConverterOptionString.write(value.createdAt, into: &buf)
}
}
@@ -763,6 +799,68 @@ public func FfiConverterTypeEpubMetadata_lower(_ value: EpubMetadata) -> RustBuf
}
+public struct EventBufferStateV2: Equatable, Hashable {
+ public var total: UInt32
+ public var pending: UInt32
+ public var exported: UInt32
+ public var failed: UInt32
+
+ // Default memberwise initializers are never public by default, so we
+ // declare one manually.
+ public init(total: UInt32, pending: UInt32, exported: UInt32, failed: UInt32) {
+ self.total = total
+ self.pending = pending
+ self.exported = exported
+ self.failed = failed
+ }
+
+
+
+
+}
+
+#if compiler(>=6)
+extension EventBufferStateV2: Sendable {}
+#endif
+
+#if swift(>=5.8)
+@_documentation(visibility: private)
+#endif
+public struct FfiConverterTypeEventBufferStateV2: FfiConverterRustBuffer {
+ public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> EventBufferStateV2 {
+ return
+ try EventBufferStateV2(
+ total: FfiConverterUInt32.read(from: &buf),
+ pending: FfiConverterUInt32.read(from: &buf),
+ exported: FfiConverterUInt32.read(from: &buf),
+ failed: FfiConverterUInt32.read(from: &buf)
+ )
+ }
+
+ public static func write(_ value: EventBufferStateV2, into buf: inout [UInt8]) {
+ FfiConverterUInt32.write(value.total, into: &buf)
+ FfiConverterUInt32.write(value.pending, into: &buf)
+ FfiConverterUInt32.write(value.exported, into: &buf)
+ FfiConverterUInt32.write(value.failed, into: &buf)
+ }
+}
+
+
+#if swift(>=5.8)
+@_documentation(visibility: private)
+#endif
+public func FfiConverterTypeEventBufferStateV2_lift(_ buf: RustBuffer) throws -> EventBufferStateV2 {
+ return try FfiConverterTypeEventBufferStateV2.lift(buf)
+}
+
+#if swift(>=5.8)
+@_documentation(visibility: private)
+#endif
+public func FfiConverterTypeEventBufferStateV2_lower(_ value: EventBufferStateV2) -> RustBuffer {
+ return FfiConverterTypeEventBufferStateV2.lower(value)
+}
+
+
public struct ImageMeta: Equatable, Hashable {
public var width: UInt32
public var height: UInt32
@@ -1003,24 +1101,104 @@ public func FfiConverterTypePdfPageText_lower(_ value: PdfPageText) -> RustBuffe
}
+public struct ReadingCheckpoint: Equatable, Hashable {
+ public var schemaVersion: UInt32
+ public var materialId: String
+ public var representationFingerprint: String
+ public var rendererKind: RendererKind
+ public var capturedAtMs: UInt64
+ public var deviceId: String
+ public var deviceSequence: UInt64
+ public var position: ReadingPosition
+
+ // Default memberwise initializers are never public by default, so we
+ // declare one manually.
+ public init(schemaVersion: UInt32, materialId: String, representationFingerprint: String, rendererKind: RendererKind, capturedAtMs: UInt64, deviceId: String, deviceSequence: UInt64, position: ReadingPosition) {
+ self.schemaVersion = schemaVersion
+ self.materialId = materialId
+ self.representationFingerprint = representationFingerprint
+ self.rendererKind = rendererKind
+ self.capturedAtMs = capturedAtMs
+ self.deviceId = deviceId
+ self.deviceSequence = deviceSequence
+ self.position = position
+ }
+
+
+
+
+}
+
+#if compiler(>=6)
+extension ReadingCheckpoint: Sendable {}
+#endif
+
+#if swift(>=5.8)
+@_documentation(visibility: private)
+#endif
+public struct FfiConverterTypeReadingCheckpoint: FfiConverterRustBuffer {
+ public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> ReadingCheckpoint {
+ return
+ try ReadingCheckpoint(
+ schemaVersion: FfiConverterUInt32.read(from: &buf),
+ materialId: FfiConverterString.read(from: &buf),
+ representationFingerprint: FfiConverterString.read(from: &buf),
+ rendererKind: FfiConverterTypeRendererKind.read(from: &buf),
+ capturedAtMs: FfiConverterUInt64.read(from: &buf),
+ deviceId: FfiConverterString.read(from: &buf),
+ deviceSequence: FfiConverterUInt64.read(from: &buf),
+ position: FfiConverterTypeReadingPosition.read(from: &buf)
+ )
+ }
+
+ public static func write(_ value: ReadingCheckpoint, into buf: inout [UInt8]) {
+ FfiConverterUInt32.write(value.schemaVersion, into: &buf)
+ FfiConverterString.write(value.materialId, into: &buf)
+ FfiConverterString.write(value.representationFingerprint, into: &buf)
+ FfiConverterTypeRendererKind.write(value.rendererKind, into: &buf)
+ FfiConverterUInt64.write(value.capturedAtMs, into: &buf)
+ FfiConverterString.write(value.deviceId, into: &buf)
+ FfiConverterUInt64.write(value.deviceSequence, into: &buf)
+ FfiConverterTypeReadingPosition.write(value.position, into: &buf)
+ }
+}
+
+
+#if swift(>=5.8)
+@_documentation(visibility: private)
+#endif
+public func FfiConverterTypeReadingCheckpoint_lift(_ buf: RustBuffer) throws -> ReadingCheckpoint {
+ return try FfiConverterTypeReadingCheckpoint.lift(buf)
+}
+
+#if swift(>=5.8)
+@_documentation(visibility: private)
+#endif
+public func FfiConverterTypeReadingCheckpoint_lower(_ value: ReadingCheckpoint) -> RustBuffer {
+ return FfiConverterTypeReadingCheckpoint.lower(value)
+}
+
+
public struct ReadingEventV2: Equatable, Hashable {
public var eventId: String
public var clientSessionId: String
public var materialId: String
public var eventType: ReadingEventTypeV2
public var position: ReadingPosition?
+ public var checkpoint: ReadingCheckpoint?
public var activeSecondsDelta: UInt32
public var timestampMs: Int64
public var sequence: UInt64
// Default memberwise initializers are never public by default, so we
// declare one manually.
- public init(eventId: String, clientSessionId: String, materialId: String, eventType: ReadingEventTypeV2, position: ReadingPosition?, activeSecondsDelta: UInt32, timestampMs: Int64, sequence: UInt64) {
+ public init(eventId: String, clientSessionId: String, materialId: String, eventType: ReadingEventTypeV2, position: ReadingPosition?, checkpoint: ReadingCheckpoint?, activeSecondsDelta: UInt32, timestampMs: Int64, sequence: UInt64) {
self.eventId = eventId
self.clientSessionId = clientSessionId
self.materialId = materialId
self.eventType = eventType
self.position = position
+ self.checkpoint = checkpoint
self.activeSecondsDelta = activeSecondsDelta
self.timestampMs = timestampMs
self.sequence = sequence
@@ -1047,6 +1225,7 @@ public struct FfiConverterTypeReadingEventV2: FfiConverterRustBuffer {
materialId: FfiConverterString.read(from: &buf),
eventType: FfiConverterTypeReadingEventTypeV2.read(from: &buf),
position: FfiConverterOptionTypeReadingPosition.read(from: &buf),
+ checkpoint: FfiConverterOptionTypeReadingCheckpoint.read(from: &buf),
activeSecondsDelta: FfiConverterUInt32.read(from: &buf),
timestampMs: FfiConverterInt64.read(from: &buf),
sequence: FfiConverterUInt64.read(from: &buf)
@@ -1059,6 +1238,7 @@ public struct FfiConverterTypeReadingEventV2: FfiConverterRustBuffer {
FfiConverterString.write(value.materialId, into: &buf)
FfiConverterTypeReadingEventTypeV2.write(value.eventType, into: &buf)
FfiConverterOptionTypeReadingPosition.write(value.position, into: &buf)
+ FfiConverterOptionTypeReadingCheckpoint.write(value.checkpoint, into: &buf)
FfiConverterUInt32.write(value.activeSecondsDelta, into: &buf)
FfiConverterInt64.write(value.timestampMs, into: &buf)
FfiConverterUInt64.write(value.sequence, into: &buf)
@@ -1131,26 +1311,108 @@ public func FfiConverterTypeReadingMaterialRef_lower(_ value: ReadingMaterialRef
}
+public struct ReadingMaterialRefV2: Equatable, Hashable {
+ public var materialId: String
+ public var filePath: String
+ public var fileName: String
+ public var fileType: String
+ public var mimeType: String?
+ public var fileSize: UInt64?
+ public var contentHash: String?
+ public var createdAtMs: Int64?
+
+ // Default memberwise initializers are never public by default, so we
+ // declare one manually.
+ public init(materialId: String, filePath: String, fileName: String, fileType: String, mimeType: String?, fileSize: UInt64?, contentHash: String?, createdAtMs: Int64?) {
+ self.materialId = materialId
+ self.filePath = filePath
+ self.fileName = fileName
+ self.fileType = fileType
+ self.mimeType = mimeType
+ self.fileSize = fileSize
+ self.contentHash = contentHash
+ self.createdAtMs = createdAtMs
+ }
+
+
+
+
+}
+
+#if compiler(>=6)
+extension ReadingMaterialRefV2: Sendable {}
+#endif
+
+#if swift(>=5.8)
+@_documentation(visibility: private)
+#endif
+public struct FfiConverterTypeReadingMaterialRefV2: FfiConverterRustBuffer {
+ public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> ReadingMaterialRefV2 {
+ return
+ try ReadingMaterialRefV2(
+ materialId: FfiConverterString.read(from: &buf),
+ filePath: FfiConverterString.read(from: &buf),
+ fileName: FfiConverterString.read(from: &buf),
+ fileType: FfiConverterString.read(from: &buf),
+ mimeType: FfiConverterOptionString.read(from: &buf),
+ fileSize: FfiConverterOptionUInt64.read(from: &buf),
+ contentHash: FfiConverterOptionString.read(from: &buf),
+ createdAtMs: FfiConverterOptionInt64.read(from: &buf)
+ )
+ }
+
+ public static func write(_ value: ReadingMaterialRefV2, into buf: inout [UInt8]) {
+ FfiConverterString.write(value.materialId, into: &buf)
+ FfiConverterString.write(value.filePath, into: &buf)
+ FfiConverterString.write(value.fileName, into: &buf)
+ FfiConverterString.write(value.fileType, into: &buf)
+ FfiConverterOptionString.write(value.mimeType, into: &buf)
+ FfiConverterOptionUInt64.write(value.fileSize, into: &buf)
+ FfiConverterOptionString.write(value.contentHash, into: &buf)
+ FfiConverterOptionInt64.write(value.createdAtMs, into: &buf)
+ }
+}
+
+
+#if swift(>=5.8)
+@_documentation(visibility: private)
+#endif
+public func FfiConverterTypeReadingMaterialRefV2_lift(_ buf: RustBuffer) throws -> ReadingMaterialRefV2 {
+ return try FfiConverterTypeReadingMaterialRefV2.lift(buf)
+}
+
+#if swift(>=5.8)
+@_documentation(visibility: private)
+#endif
+public func FfiConverterTypeReadingMaterialRefV2_lower(_ value: ReadingMaterialRefV2) -> RustBuffer {
+ return FfiConverterTypeReadingMaterialRefV2.lower(value)
+}
+
+
public struct ReadingSessionV2: Equatable, Hashable {
public var clientSessionId: String
- public var material: ReadingMaterialRef
+ public var material: ReadingMaterialRefV2
public var startedAtMs: Int64
+ public var closedAtMs: Int64?
public var lastEventAtMs: Int64
public var nextSequence: UInt64
public var totalActiveSeconds: UInt32
public var lastPosition: ReadingPosition?
+ public var lastCheckpoint: ReadingCheckpoint?
public var status: ReadingSessionStatus
// Default memberwise initializers are never public by default, so we
// declare one manually.
- public init(clientSessionId: String, material: ReadingMaterialRef, startedAtMs: Int64, lastEventAtMs: Int64, nextSequence: UInt64, totalActiveSeconds: UInt32, lastPosition: ReadingPosition?, status: ReadingSessionStatus) {
+ public init(clientSessionId: String, material: ReadingMaterialRefV2, startedAtMs: Int64, closedAtMs: Int64?, lastEventAtMs: Int64, nextSequence: UInt64, totalActiveSeconds: UInt32, lastPosition: ReadingPosition?, lastCheckpoint: ReadingCheckpoint?, status: ReadingSessionStatus) {
self.clientSessionId = clientSessionId
self.material = material
self.startedAtMs = startedAtMs
+ self.closedAtMs = closedAtMs
self.lastEventAtMs = lastEventAtMs
self.nextSequence = nextSequence
self.totalActiveSeconds = totalActiveSeconds
self.lastPosition = lastPosition
+ self.lastCheckpoint = lastCheckpoint
self.status = status
}
@@ -1171,24 +1433,28 @@ public struct FfiConverterTypeReadingSessionV2: FfiConverterRustBuffer {
return
try ReadingSessionV2(
clientSessionId: FfiConverterString.read(from: &buf),
- material: FfiConverterTypeReadingMaterialRef.read(from: &buf),
+ material: FfiConverterTypeReadingMaterialRefV2.read(from: &buf),
startedAtMs: FfiConverterInt64.read(from: &buf),
+ closedAtMs: FfiConverterOptionInt64.read(from: &buf),
lastEventAtMs: FfiConverterInt64.read(from: &buf),
nextSequence: FfiConverterUInt64.read(from: &buf),
totalActiveSeconds: FfiConverterUInt32.read(from: &buf),
lastPosition: FfiConverterOptionTypeReadingPosition.read(from: &buf),
+ lastCheckpoint: FfiConverterOptionTypeReadingCheckpoint.read(from: &buf),
status: FfiConverterTypeReadingSessionStatus.read(from: &buf)
)
}
public static func write(_ value: ReadingSessionV2, into buf: inout [UInt8]) {
FfiConverterString.write(value.clientSessionId, into: &buf)
- FfiConverterTypeReadingMaterialRef.write(value.material, into: &buf)
+ FfiConverterTypeReadingMaterialRefV2.write(value.material, into: &buf)
FfiConverterInt64.write(value.startedAtMs, into: &buf)
+ FfiConverterOptionInt64.write(value.closedAtMs, into: &buf)
FfiConverterInt64.write(value.lastEventAtMs, into: &buf)
FfiConverterUInt64.write(value.nextSequence, into: &buf)
FfiConverterUInt32.write(value.totalActiveSeconds, into: &buf)
FfiConverterOptionTypeReadingPosition.write(value.lastPosition, into: &buf)
+ FfiConverterOptionTypeReadingCheckpoint.write(value.lastCheckpoint, into: &buf)
FfiConverterTypeReadingSessionStatus.write(value.status, into: &buf)
}
}
@@ -1496,6 +1762,16 @@ public enum DocumentError: Swift.Error, Equatable, Hashable, Foundation.Localize
case IoError(message: String)
+ case SessionNotFound(message: String)
+
+ case SessionAlreadyActive(message: String)
+
+ case SessionNotActive(message: String)
+
+ case BufferOverflow(message: String)
+
+ case Unsupported(message: String)
+
@@ -1545,6 +1821,26 @@ public struct FfiConverterTypeDocumentError: FfiConverterRustBuffer {
message: try FfiConverterString.read(from: &buf)
)
+ case 6: return .SessionNotFound(
+ message: try FfiConverterString.read(from: &buf)
+ )
+
+ case 7: return .SessionAlreadyActive(
+ message: try FfiConverterString.read(from: &buf)
+ )
+
+ case 8: return .SessionNotActive(
+ message: try FfiConverterString.read(from: &buf)
+ )
+
+ case 9: return .BufferOverflow(
+ message: try FfiConverterString.read(from: &buf)
+ )
+
+ case 10: return .Unsupported(
+ message: try FfiConverterString.read(from: &buf)
+ )
+
default: throw UniffiInternalError.unexpectedEnumCase
}
@@ -1566,6 +1862,16 @@ public struct FfiConverterTypeDocumentError: FfiConverterRustBuffer {
writeInt(&buf, Int32(4))
case .IoError(_ /* message is ignored*/):
writeInt(&buf, Int32(5))
+ case .SessionNotFound(_ /* message is ignored*/):
+ writeInt(&buf, Int32(6))
+ case .SessionAlreadyActive(_ /* message is ignored*/):
+ writeInt(&buf, Int32(7))
+ case .SessionNotActive(_ /* message is ignored*/):
+ writeInt(&buf, Int32(8))
+ case .BufferOverflow(_ /* message is ignored*/):
+ writeInt(&buf, Int32(9))
+ case .Unsupported(_ /* message is ignored*/):
+ writeInt(&buf, Int32(10))
}
@@ -2218,14 +2524,24 @@ public func FfiConverterTypeReadingEventTypeV2_lower(_ value: ReadingEventTypeV2
public enum ReadingPosition: Equatable, Hashable {
+ case block(blockId: String, blockProgress: Float, overallProgress: Float
+ )
case markdown(blockId: String, scrollProgress: Float
)
case text(lineNumber: UInt32, scrollProgress: Float
)
case pdf(pageNumber: UInt32, pageProgress: Float, overallProgress: Float
)
+ case pdfViewport(pageNumber: UInt32, anchorX: Float, anchorY: Float, zoomScale: Float, overallProgress: Float
+ )
case image(zoomScale: Float, offsetX: Float, offsetY: Float
)
+ case imageViewport(zoomScale: Float, anchorX: Float, anchorY: Float
+ )
+ case workbook(sheetId: String, topRow: UInt32, leftColumn: UInt32, selectedCell: String?, zoomScale: Float, overallProgress: Float?
+ )
+ case slide(slideIndex: UInt32, zoomScale: Float, anchorX: Float, anchorY: Float, overallProgress: Float
+ )
case epub(chapterId: String, chapterProgress: Float, overallProgress: Float
)
case unknown
@@ -2250,22 +2566,37 @@ public struct FfiConverterTypeReadingPosition: FfiConverterRustBuffer {
let variant: Int32 = try readInt(&buf)
switch variant {
- case 1: return .markdown(blockId: try FfiConverterString.read(from: &buf), scrollProgress: try FfiConverterFloat.read(from: &buf)
+ case 1: return .block(blockId: try FfiConverterString.read(from: &buf), blockProgress: try FfiConverterFloat.read(from: &buf), overallProgress: try FfiConverterFloat.read(from: &buf)
)
- case 2: return .text(lineNumber: try FfiConverterUInt32.read(from: &buf), scrollProgress: try FfiConverterFloat.read(from: &buf)
+ case 2: return .markdown(blockId: try FfiConverterString.read(from: &buf), scrollProgress: try FfiConverterFloat.read(from: &buf)
)
- case 3: return .pdf(pageNumber: try FfiConverterUInt32.read(from: &buf), pageProgress: try FfiConverterFloat.read(from: &buf), overallProgress: try FfiConverterFloat.read(from: &buf)
+ case 3: return .text(lineNumber: try FfiConverterUInt32.read(from: &buf), scrollProgress: try FfiConverterFloat.read(from: &buf)
)
- case 4: return .image(zoomScale: try FfiConverterFloat.read(from: &buf), offsetX: try FfiConverterFloat.read(from: &buf), offsetY: try FfiConverterFloat.read(from: &buf)
+ case 4: return .pdf(pageNumber: try FfiConverterUInt32.read(from: &buf), pageProgress: try FfiConverterFloat.read(from: &buf), overallProgress: try FfiConverterFloat.read(from: &buf)
)
- case 5: return .epub(chapterId: try FfiConverterString.read(from: &buf), chapterProgress: try FfiConverterFloat.read(from: &buf), overallProgress: try FfiConverterFloat.read(from: &buf)
+ case 5: return .pdfViewport(pageNumber: try FfiConverterUInt32.read(from: &buf), anchorX: try FfiConverterFloat.read(from: &buf), anchorY: try FfiConverterFloat.read(from: &buf), zoomScale: try FfiConverterFloat.read(from: &buf), overallProgress: try FfiConverterFloat.read(from: &buf)
)
- case 6: return .unknown
+ case 6: return .image(zoomScale: try FfiConverterFloat.read(from: &buf), offsetX: try FfiConverterFloat.read(from: &buf), offsetY: try FfiConverterFloat.read(from: &buf)
+ )
+
+ case 7: return .imageViewport(zoomScale: try FfiConverterFloat.read(from: &buf), anchorX: try FfiConverterFloat.read(from: &buf), anchorY: try FfiConverterFloat.read(from: &buf)
+ )
+
+ case 8: return .workbook(sheetId: try FfiConverterString.read(from: &buf), topRow: try FfiConverterUInt32.read(from: &buf), leftColumn: try FfiConverterUInt32.read(from: &buf), selectedCell: try FfiConverterOptionString.read(from: &buf), zoomScale: try FfiConverterFloat.read(from: &buf), overallProgress: try FfiConverterOptionFloat.read(from: &buf)
+ )
+
+ case 9: return .slide(slideIndex: try FfiConverterUInt32.read(from: &buf), zoomScale: try FfiConverterFloat.read(from: &buf), anchorX: try FfiConverterFloat.read(from: &buf), anchorY: try FfiConverterFloat.read(from: &buf), overallProgress: try FfiConverterFloat.read(from: &buf)
+ )
+
+ case 10: return .epub(chapterId: try FfiConverterString.read(from: &buf), chapterProgress: try FfiConverterFloat.read(from: &buf), overallProgress: try FfiConverterFloat.read(from: &buf)
+ )
+
+ case 11: return .unknown
default: throw UniffiInternalError.unexpectedEnumCase
}
@@ -2275,41 +2606,83 @@ public struct FfiConverterTypeReadingPosition: FfiConverterRustBuffer {
switch value {
- case let .markdown(blockId,scrollProgress):
+ case let .block(blockId,blockProgress,overallProgress):
writeInt(&buf, Int32(1))
FfiConverterString.write(blockId, into: &buf)
+ FfiConverterFloat.write(blockProgress, into: &buf)
+ FfiConverterFloat.write(overallProgress, into: &buf)
+
+
+ case let .markdown(blockId,scrollProgress):
+ writeInt(&buf, Int32(2))
+ FfiConverterString.write(blockId, into: &buf)
FfiConverterFloat.write(scrollProgress, into: &buf)
case let .text(lineNumber,scrollProgress):
- writeInt(&buf, Int32(2))
+ writeInt(&buf, Int32(3))
FfiConverterUInt32.write(lineNumber, into: &buf)
FfiConverterFloat.write(scrollProgress, into: &buf)
case let .pdf(pageNumber,pageProgress,overallProgress):
- writeInt(&buf, Int32(3))
+ writeInt(&buf, Int32(4))
FfiConverterUInt32.write(pageNumber, into: &buf)
FfiConverterFloat.write(pageProgress, into: &buf)
FfiConverterFloat.write(overallProgress, into: &buf)
+ case let .pdfViewport(pageNumber,anchorX,anchorY,zoomScale,overallProgress):
+ writeInt(&buf, Int32(5))
+ FfiConverterUInt32.write(pageNumber, into: &buf)
+ FfiConverterFloat.write(anchorX, into: &buf)
+ FfiConverterFloat.write(anchorY, into: &buf)
+ FfiConverterFloat.write(zoomScale, into: &buf)
+ FfiConverterFloat.write(overallProgress, into: &buf)
+
+
case let .image(zoomScale,offsetX,offsetY):
- writeInt(&buf, Int32(4))
+ writeInt(&buf, Int32(6))
FfiConverterFloat.write(zoomScale, into: &buf)
FfiConverterFloat.write(offsetX, into: &buf)
FfiConverterFloat.write(offsetY, into: &buf)
+ case let .imageViewport(zoomScale,anchorX,anchorY):
+ writeInt(&buf, Int32(7))
+ FfiConverterFloat.write(zoomScale, into: &buf)
+ FfiConverterFloat.write(anchorX, into: &buf)
+ FfiConverterFloat.write(anchorY, into: &buf)
+
+
+ case let .workbook(sheetId,topRow,leftColumn,selectedCell,zoomScale,overallProgress):
+ writeInt(&buf, Int32(8))
+ FfiConverterString.write(sheetId, into: &buf)
+ FfiConverterUInt32.write(topRow, into: &buf)
+ FfiConverterUInt32.write(leftColumn, into: &buf)
+ FfiConverterOptionString.write(selectedCell, into: &buf)
+ FfiConverterFloat.write(zoomScale, into: &buf)
+ FfiConverterOptionFloat.write(overallProgress, into: &buf)
+
+
+ case let .slide(slideIndex,zoomScale,anchorX,anchorY,overallProgress):
+ writeInt(&buf, Int32(9))
+ FfiConverterUInt32.write(slideIndex, into: &buf)
+ FfiConverterFloat.write(zoomScale, into: &buf)
+ FfiConverterFloat.write(anchorX, into: &buf)
+ FfiConverterFloat.write(anchorY, into: &buf)
+ FfiConverterFloat.write(overallProgress, into: &buf)
+
+
case let .epub(chapterId,chapterProgress,overallProgress):
- writeInt(&buf, Int32(5))
+ writeInt(&buf, Int32(10))
FfiConverterString.write(chapterId, into: &buf)
FfiConverterFloat.write(chapterProgress, into: &buf)
FfiConverterFloat.write(overallProgress, into: &buf)
case .unknown:
- writeInt(&buf, Int32(6))
+ writeInt(&buf, Int32(11))
}
}
@@ -2339,6 +2712,8 @@ public enum ReadingSessionStatus: Equatable, Hashable {
case active
case paused
case closed
+ case interrupted
+ case failed
@@ -2366,6 +2741,10 @@ public struct FfiConverterTypeReadingSessionStatus: FfiConverterRustBuffer {
case 3: return .closed
+ case 4: return .interrupted
+
+ case 5: return .failed
+
default: throw UniffiInternalError.unexpectedEnumCase
}
}
@@ -2385,6 +2764,14 @@ public struct FfiConverterTypeReadingSessionStatus: FfiConverterRustBuffer {
case .closed:
writeInt(&buf, Int32(3))
+
+ case .interrupted:
+ writeInt(&buf, Int32(4))
+
+
+ case .failed:
+ writeInt(&buf, Int32(5))
+
}
}
}
@@ -2405,6 +2792,108 @@ public func FfiConverterTypeReadingSessionStatus_lower(_ value: ReadingSessionSt
}
+// Note that we don't yet support `indirect` for enums.
+// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion.
+
+public enum RendererKind: Equatable, Hashable {
+
+ case block
+ case pdf
+ case image
+ case workbook
+ case slide
+ case epub
+ case unknown
+
+
+
+
+
+}
+
+#if compiler(>=6)
+extension RendererKind: Sendable {}
+#endif
+
+#if swift(>=5.8)
+@_documentation(visibility: private)
+#endif
+public struct FfiConverterTypeRendererKind: FfiConverterRustBuffer {
+ typealias SwiftType = RendererKind
+
+ public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> RendererKind {
+ let variant: Int32 = try readInt(&buf)
+ switch variant {
+
+ case 1: return .block
+
+ case 2: return .pdf
+
+ case 3: return .image
+
+ case 4: return .workbook
+
+ case 5: return .slide
+
+ case 6: return .epub
+
+ case 7: return .unknown
+
+ default: throw UniffiInternalError.unexpectedEnumCase
+ }
+ }
+
+ public static func write(_ value: RendererKind, into buf: inout [UInt8]) {
+ switch value {
+
+
+ case .block:
+ writeInt(&buf, Int32(1))
+
+
+ case .pdf:
+ writeInt(&buf, Int32(2))
+
+
+ case .image:
+ writeInt(&buf, Int32(3))
+
+
+ case .workbook:
+ writeInt(&buf, Int32(4))
+
+
+ case .slide:
+ writeInt(&buf, Int32(5))
+
+
+ case .epub:
+ writeInt(&buf, Int32(6))
+
+
+ case .unknown:
+ writeInt(&buf, Int32(7))
+
+ }
+ }
+}
+
+
+#if swift(>=5.8)
+@_documentation(visibility: private)
+#endif
+public func FfiConverterTypeRendererKind_lift(_ buf: RustBuffer) throws -> RendererKind {
+ return try FfiConverterTypeRendererKind.lift(buf)
+}
+
+#if swift(>=5.8)
+@_documentation(visibility: private)
+#endif
+public func FfiConverterTypeRendererKind_lower(_ value: RendererKind) -> RustBuffer {
+ return FfiConverterTypeRendererKind.lower(value)
+}
+
+
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
@@ -2429,6 +2918,78 @@ fileprivate struct FfiConverterOptionUInt32: FfiConverterRustBuffer {
}
}
+#if swift(>=5.8)
+@_documentation(visibility: private)
+#endif
+fileprivate struct FfiConverterOptionUInt64: FfiConverterRustBuffer {
+ typealias SwiftType = UInt64?
+
+ public static func write(_ value: SwiftType, into buf: inout [UInt8]) {
+ guard let value = value else {
+ writeInt(&buf, Int8(0))
+ return
+ }
+ writeInt(&buf, Int8(1))
+ FfiConverterUInt64.write(value, into: &buf)
+ }
+
+ public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType {
+ switch try readInt(&buf) as Int8 {
+ case 0: return nil
+ case 1: return try FfiConverterUInt64.read(from: &buf)
+ default: throw UniffiInternalError.unexpectedOptionalTag
+ }
+ }
+}
+
+#if swift(>=5.8)
+@_documentation(visibility: private)
+#endif
+fileprivate struct FfiConverterOptionInt64: FfiConverterRustBuffer {
+ typealias SwiftType = Int64?
+
+ public static func write(_ value: SwiftType, into buf: inout [UInt8]) {
+ guard let value = value else {
+ writeInt(&buf, Int8(0))
+ return
+ }
+ writeInt(&buf, Int8(1))
+ FfiConverterInt64.write(value, into: &buf)
+ }
+
+ public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType {
+ switch try readInt(&buf) as Int8 {
+ case 0: return nil
+ case 1: return try FfiConverterInt64.read(from: &buf)
+ default: throw UniffiInternalError.unexpectedOptionalTag
+ }
+ }
+}
+
+#if swift(>=5.8)
+@_documentation(visibility: private)
+#endif
+fileprivate struct FfiConverterOptionFloat: FfiConverterRustBuffer {
+ typealias SwiftType = Float?
+
+ public static func write(_ value: SwiftType, into buf: inout [UInt8]) {
+ guard let value = value else {
+ writeInt(&buf, Int8(0))
+ return
+ }
+ writeInt(&buf, Int8(1))
+ FfiConverterFloat.write(value, into: &buf)
+ }
+
+ public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType {
+ switch try readInt(&buf) as Int8 {
+ case 0: return nil
+ case 1: return try FfiConverterFloat.read(from: &buf)
+ default: throw UniffiInternalError.unexpectedOptionalTag
+ }
+ }
+}
+
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
@@ -2453,6 +3014,54 @@ fileprivate struct FfiConverterOptionString: FfiConverterRustBuffer {
}
}
+#if swift(>=5.8)
+@_documentation(visibility: private)
+#endif
+fileprivate struct FfiConverterOptionTypeReadingCheckpoint: FfiConverterRustBuffer {
+ typealias SwiftType = ReadingCheckpoint?
+
+ public static func write(_ value: SwiftType, into buf: inout [UInt8]) {
+ guard let value = value else {
+ writeInt(&buf, Int8(0))
+ return
+ }
+ writeInt(&buf, Int8(1))
+ FfiConverterTypeReadingCheckpoint.write(value, into: &buf)
+ }
+
+ public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType {
+ switch try readInt(&buf) as Int8 {
+ case 0: return nil
+ case 1: return try FfiConverterTypeReadingCheckpoint.read(from: &buf)
+ default: throw UniffiInternalError.unexpectedOptionalTag
+ }
+ }
+}
+
+#if swift(>=5.8)
+@_documentation(visibility: private)
+#endif
+fileprivate struct FfiConverterOptionTypeReadingSessionV2: FfiConverterRustBuffer {
+ typealias SwiftType = ReadingSessionV2?
+
+ public static func write(_ value: SwiftType, into buf: inout [UInt8]) {
+ guard let value = value else {
+ writeInt(&buf, Int8(0))
+ return
+ }
+ writeInt(&buf, Int8(1))
+ FfiConverterTypeReadingSessionV2.write(value, into: &buf)
+ }
+
+ public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType {
+ switch try readInt(&buf) as Int8 {
+ case 0: return nil
+ case 1: return try FfiConverterTypeReadingSessionV2.read(from: &buf)
+ default: throw UniffiInternalError.unexpectedOptionalTag
+ }
+ }
+}
+
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
@@ -2708,6 +3317,16 @@ public func ackEventsV2(eventIds: [String]) -> UInt32 {
)
})
}
+public func buildRepresentationFingerprint(sourceFingerprint: String, rendererKind: RendererKind, parserVersion: String, schemaVersion: UInt32) -> String {
+ return try! FfiConverterString.lift(try! rustCall() {
+ uniffi_zx_document_ffi_fn_func_build_representation_fingerprint(
+ FfiConverterString.lower(sourceFingerprint),
+ FfiConverterTypeRendererKind_lower(rendererKind),
+ FfiConverterString.lower(parserVersion),
+ FfiConverterUInt32.lower(schemaVersion),$0
+ )
+})
+}
public func cleanupStaleSessionsFfi(nowMs: Int64, maxAgeMs: Int64) -> UInt32 {
return try! FfiConverterUInt32.lift(try! rustCall() {
uniffi_zx_document_ffi_fn_func_cleanup_stale_sessions_ffi(
@@ -2722,12 +3341,20 @@ public func clearExportedEvents(count: UInt32) {try! rustCall() {
)
}
}
-public func closeReadingSessionV2(sessionId: String)throws {try rustCallWithError(FfiConverterTypeDocumentError_lift) {
+public func closeReadingSessionV2(sessionId: String, timestampMs: Int64)throws {try rustCallWithError(FfiConverterTypeDocumentError_lift) {
uniffi_zx_document_ffi_fn_func_close_reading_session_v2(
- FfiConverterString.lower(sessionId),$0
+ FfiConverterString.lower(sessionId),
+ FfiConverterInt64.lower(timestampMs),$0
)
}
}
+public func computeFileFingerprint(filePath: String)throws -> String {
+ return try FfiConverterString.lift(try rustCallWithError(FfiConverterTypeDocumentError_lift) {
+ uniffi_zx_document_ffi_fn_func_compute_file_fingerprint(
+ FfiConverterString.lower(filePath),$0
+ )
+})
+}
public func createNoteAnchor(materialId: String, position: ReadingPosition?) -> NoteAnchor {
return try! FfiConverterTypeNoteAnchor_lift(try! rustCall() {
uniffi_zx_document_ffi_fn_func_create_note_anchor(
@@ -2772,6 +3399,19 @@ public func extractPdfTextFfi(filePath: String)throws -> [PdfPageText] {
)
})
}
+public func getActiveSessionV2(materialId: String) -> ReadingSessionV2? {
+ return try! FfiConverterOptionTypeReadingSessionV2.lift(try! rustCall() {
+ uniffi_zx_document_ffi_fn_func_get_active_session_v2(
+ FfiConverterString.lower(materialId),$0
+ )
+})
+}
+public func getEventBufferStateV2() -> EventBufferStateV2 {
+ return try! FfiConverterTypeEventBufferStateV2_lift(try! rustCall() {
+ uniffi_zx_document_ffi_fn_func_get_event_buffer_state_v2($0
+ )
+})
+}
public func getOfficePreviewConfigFfi(materialType: MaterialType, fileSize: UInt64)throws -> OfficePreviewConfig {
return try FfiConverterTypeOfficePreviewConfig_lift(try rustCallWithError(FfiConverterTypeDocumentError_lift) {
uniffi_zx_document_ffi_fn_func_get_office_preview_config_ffi(
@@ -2780,6 +3420,13 @@ public func getOfficePreviewConfigFfi(materialType: MaterialType, fileSize: UInt
)
})
}
+public func getSessionV2Ffi(sessionId: String) -> ReadingSessionV2? {
+ return try! FfiConverterOptionTypeReadingSessionV2.lift(try! rustCall() {
+ uniffi_zx_document_ffi_fn_func_get_session_v2_ffi(
+ FfiConverterString.lower(sessionId),$0
+ )
+})
+}
public func isOfficeTypeFfi(materialType: MaterialType) -> Bool {
return try! FfiConverterBool.lift(try! rustCall() {
uniffi_zx_document_ffi_fn_func_is_office_type_ffi(
@@ -2794,6 +3441,18 @@ public func markEventsFailedV2(eventIds: [String]) -> UInt32 {
)
})
}
+public func markSessionFailedV2(sessionId: String)throws {try rustCallWithError(FfiConverterTypeDocumentError_lift) {
+ uniffi_zx_document_ffi_fn_func_mark_session_failed_v2(
+ FfiConverterString.lower(sessionId),$0
+ )
+}
+}
+public func markSessionInterruptedV2(sessionId: String)throws {try rustCallWithError(FfiConverterTypeDocumentError_lift) {
+ uniffi_zx_document_ffi_fn_func_mark_session_interrupted_v2(
+ FfiConverterString.lower(sessionId),$0
+ )
+}
+}
public func parseMarkdown(content: String)throws -> [DocumentBlock] {
return try FfiConverterSequenceTypeDocumentBlock.lift(try rustCallWithError(FfiConverterTypeDocumentError_lift) {
uniffi_zx_document_ffi_fn_func_parse_markdown(
@@ -2814,6 +3473,27 @@ public func pauseReadingSessionV2(sessionId: String)throws {try rustCallWithEr
)
}
}
+public func pushCheckpointChangedV2(sessionId: String, materialId: String, checkpoint: ReadingCheckpoint, timestampMs: Int64)throws -> ReadingEventV2 {
+ return try FfiConverterTypeReadingEventV2_lift(try rustCallWithError(FfiConverterTypeDocumentError_lift) {
+ uniffi_zx_document_ffi_fn_func_push_checkpoint_changed_v2(
+ FfiConverterString.lower(sessionId),
+ FfiConverterString.lower(materialId),
+ FfiConverterTypeReadingCheckpoint_lower(checkpoint),
+ FfiConverterInt64.lower(timestampMs),$0
+ )
+})
+}
+public func pushHeartbeatCheckpointV2(sessionId: String, materialId: String, activeSecondsDelta: UInt32, checkpoint: ReadingCheckpoint?, timestampMs: Int64)throws -> ReadingEventV2 {
+ return try FfiConverterTypeReadingEventV2_lift(try rustCallWithError(FfiConverterTypeDocumentError_lift) {
+ uniffi_zx_document_ffi_fn_func_push_heartbeat_checkpoint_v2(
+ FfiConverterString.lower(sessionId),
+ FfiConverterString.lower(materialId),
+ FfiConverterUInt32.lower(activeSecondsDelta),
+ FfiConverterOptionTypeReadingCheckpoint.lower(checkpoint),
+ FfiConverterInt64.lower(timestampMs),$0
+ )
+})
+}
public func pushHeartbeatV2(sessionId: String, materialId: String, activeSecondsDelta: UInt32, position: ReadingPosition?, timestampMs: Int64)throws -> ReadingEventV2 {
return try FfiConverterTypeReadingEventV2_lift(try rustCallWithError(FfiConverterTypeDocumentError_lift) {
uniffi_zx_document_ffi_fn_func_push_heartbeat_v2(
@@ -2957,10 +3637,17 @@ public func searchTextContent(content: String, query: String) -> [SearchResult]
)
})
}
-public func startReadingSessionV2(material: ReadingMaterialRef, timestampMs: Int64)throws -> String {
+public func setSessionClosedAtMsV2(sessionId: String, timestampMs: Int64)throws {try rustCallWithError(FfiConverterTypeDocumentError_lift) {
+ uniffi_zx_document_ffi_fn_func_set_session_closed_at_ms_v2(
+ FfiConverterString.lower(sessionId),
+ FfiConverterInt64.lower(timestampMs),$0
+ )
+}
+}
+public func startReadingSessionV2(material: ReadingMaterialRefV2, timestampMs: Int64)throws -> String {
return try FfiConverterString.lift(try rustCallWithError(FfiConverterTypeDocumentError_lift) {
uniffi_zx_document_ffi_fn_func_start_reading_session_v2(
- FfiConverterTypeReadingMaterialRef_lower(material),
+ FfiConverterTypeReadingMaterialRefV2_lower(material),
FfiConverterInt64.lower(timestampMs),$0
)
})
diff --git a/bindings/ios/generated/zx_documentFFI.h b/bindings/ios/generated/zx_documentFFI.h
index 04d1257..4073836 100644
--- a/bindings/ios/generated/zx_documentFFI.h
+++ b/bindings/ios/generated/zx_documentFFI.h
@@ -248,6 +248,11 @@ typedef void (*UniffiForeignFutureCompleteVoid)(uint64_t, UniffiForeignFutureRes
uint32_t uniffi_zx_document_ffi_fn_func_ack_events_v2(RustBuffer event_ids, RustCallStatus *_Nonnull out_status
);
#endif
+#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_BUILD_REPRESENTATION_FINGERPRINT
+#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_BUILD_REPRESENTATION_FINGERPRINT
+RustBuffer uniffi_zx_document_ffi_fn_func_build_representation_fingerprint(RustBuffer source_fingerprint, RustBuffer renderer_kind, RustBuffer parser_version, uint32_t schema_version, RustCallStatus *_Nonnull out_status
+);
+#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLEANUP_STALE_SESSIONS_FFI
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLEANUP_STALE_SESSIONS_FFI
uint32_t uniffi_zx_document_ffi_fn_func_cleanup_stale_sessions_ffi(int64_t now_ms, int64_t max_age_ms, RustCallStatus *_Nonnull out_status
@@ -260,7 +265,12 @@ void uniffi_zx_document_ffi_fn_func_clear_exported_events(uint32_t count, RustCa
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLOSE_READING_SESSION_V2
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CLOSE_READING_SESSION_V2
-void uniffi_zx_document_ffi_fn_func_close_reading_session_v2(RustBuffer session_id, RustCallStatus *_Nonnull out_status
+void uniffi_zx_document_ffi_fn_func_close_reading_session_v2(RustBuffer session_id, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status
+);
+#endif
+#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_COMPUTE_FILE_FINGERPRINT
+#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_COMPUTE_FILE_FINGERPRINT
+RustBuffer uniffi_zx_document_ffi_fn_func_compute_file_fingerprint(RustBuffer file_path, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_CREATE_NOTE_ANCHOR
@@ -292,6 +302,17 @@ RustBuffer uniffi_zx_document_ffi_fn_func_export_pending_events_v2(uint32_t limi
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_EXTRACT_PDF_TEXT_FFI
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_EXTRACT_PDF_TEXT_FFI
RustBuffer uniffi_zx_document_ffi_fn_func_extract_pdf_text_ffi(RustBuffer file_path, RustCallStatus *_Nonnull out_status
+);
+#endif
+#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_GET_ACTIVE_SESSION_V2
+#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_GET_ACTIVE_SESSION_V2
+RustBuffer uniffi_zx_document_ffi_fn_func_get_active_session_v2(RustBuffer material_id, RustCallStatus *_Nonnull out_status
+);
+#endif
+#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_GET_EVENT_BUFFER_STATE_V2
+#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_GET_EVENT_BUFFER_STATE_V2
+RustBuffer uniffi_zx_document_ffi_fn_func_get_event_buffer_state_v2(RustCallStatus *_Nonnull out_status
+
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_GET_OFFICE_PREVIEW_CONFIG_FFI
@@ -299,6 +320,11 @@ RustBuffer uniffi_zx_document_ffi_fn_func_extract_pdf_text_ffi(RustBuffer file_p
RustBuffer uniffi_zx_document_ffi_fn_func_get_office_preview_config_ffi(RustBuffer material_type, uint64_t file_size, RustCallStatus *_Nonnull out_status
);
#endif
+#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_GET_SESSION_V2_FFI
+#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_GET_SESSION_V2_FFI
+RustBuffer uniffi_zx_document_ffi_fn_func_get_session_v2_ffi(RustBuffer session_id, RustCallStatus *_Nonnull out_status
+);
+#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_IS_OFFICE_TYPE_FFI
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_IS_OFFICE_TYPE_FFI
int8_t uniffi_zx_document_ffi_fn_func_is_office_type_ffi(RustBuffer material_type, RustCallStatus *_Nonnull out_status
@@ -309,6 +335,16 @@ int8_t uniffi_zx_document_ffi_fn_func_is_office_type_ffi(RustBuffer material_typ
uint32_t uniffi_zx_document_ffi_fn_func_mark_events_failed_v2(RustBuffer event_ids, RustCallStatus *_Nonnull out_status
);
#endif
+#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_MARK_SESSION_FAILED_V2
+#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_MARK_SESSION_FAILED_V2
+void uniffi_zx_document_ffi_fn_func_mark_session_failed_v2(RustBuffer session_id, RustCallStatus *_Nonnull out_status
+);
+#endif
+#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_MARK_SESSION_INTERRUPTED_V2
+#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_MARK_SESSION_INTERRUPTED_V2
+void uniffi_zx_document_ffi_fn_func_mark_session_interrupted_v2(RustBuffer session_id, RustCallStatus *_Nonnull out_status
+);
+#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PARSE_MARKDOWN
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PARSE_MARKDOWN
RustBuffer uniffi_zx_document_ffi_fn_func_parse_markdown(RustBuffer content, RustCallStatus *_Nonnull out_status
@@ -324,6 +360,16 @@ RustBuffer uniffi_zx_document_ffi_fn_func_parse_text(RustBuffer content, RustCal
void uniffi_zx_document_ffi_fn_func_pause_reading_session_v2(RustBuffer session_id, RustCallStatus *_Nonnull out_status
);
#endif
+#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_CHECKPOINT_CHANGED_V2
+#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_CHECKPOINT_CHANGED_V2
+RustBuffer uniffi_zx_document_ffi_fn_func_push_checkpoint_changed_v2(RustBuffer session_id, RustBuffer material_id, RustBuffer checkpoint, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status
+);
+#endif
+#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_HEARTBEAT_CHECKPOINT_V2
+#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_HEARTBEAT_CHECKPOINT_V2
+RustBuffer uniffi_zx_document_ffi_fn_func_push_heartbeat_checkpoint_v2(RustBuffer session_id, RustBuffer material_id, uint32_t active_seconds_delta, RustBuffer checkpoint, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status
+);
+#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_HEARTBEAT_V2
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_PUSH_HEARTBEAT_V2
RustBuffer uniffi_zx_document_ffi_fn_func_push_heartbeat_v2(RustBuffer session_id, RustBuffer material_id, uint32_t active_seconds_delta, RustBuffer position, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status
@@ -415,6 +461,11 @@ RustBuffer uniffi_zx_document_ffi_fn_func_search_pdf_pages(RustBuffer page_numbe
RustBuffer uniffi_zx_document_ffi_fn_func_search_text_content(RustBuffer content, RustBuffer query, RustCallStatus *_Nonnull out_status
);
#endif
+#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_SET_SESSION_CLOSED_AT_MS_V2
+#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_SET_SESSION_CLOSED_AT_MS_V2
+void uniffi_zx_document_ffi_fn_func_set_session_closed_at_ms_v2(RustBuffer session_id, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status
+);
+#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_START_READING_SESSION_V2
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_FN_FUNC_START_READING_SESSION_V2
RustBuffer uniffi_zx_document_ffi_fn_func_start_reading_session_v2(RustBuffer material, int64_t timestamp_ms, RustCallStatus *_Nonnull out_status
@@ -689,6 +740,12 @@ void ffi_zx_document_ffi_rust_future_complete_void(uint64_t handle, RustCallStat
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_ACK_EVENTS_V2
uint16_t uniffi_zx_document_ffi_checksum_func_ack_events_v2(void
+);
+#endif
+#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_BUILD_REPRESENTATION_FINGERPRINT
+#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_BUILD_REPRESENTATION_FINGERPRINT
+uint16_t uniffi_zx_document_ffi_checksum_func_build_representation_fingerprint(void
+
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CLEANUP_STALE_SESSIONS_FFI
@@ -707,6 +764,12 @@ uint16_t uniffi_zx_document_ffi_checksum_func_clear_exported_events(void
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CLOSE_READING_SESSION_V2
uint16_t uniffi_zx_document_ffi_checksum_func_close_reading_session_v2(void
+);
+#endif
+#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_COMPUTE_FILE_FINGERPRINT
+#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_COMPUTE_FILE_FINGERPRINT
+uint16_t uniffi_zx_document_ffi_checksum_func_compute_file_fingerprint(void
+
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_CREATE_NOTE_ANCHOR
@@ -743,12 +806,30 @@ uint16_t uniffi_zx_document_ffi_checksum_func_export_pending_events_v2(void
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_EXTRACT_PDF_TEXT_FFI
uint16_t uniffi_zx_document_ffi_checksum_func_extract_pdf_text_ffi(void
+);
+#endif
+#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_GET_ACTIVE_SESSION_V2
+#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_GET_ACTIVE_SESSION_V2
+uint16_t uniffi_zx_document_ffi_checksum_func_get_active_session_v2(void
+
+);
+#endif
+#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_GET_EVENT_BUFFER_STATE_V2
+#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_GET_EVENT_BUFFER_STATE_V2
+uint16_t uniffi_zx_document_ffi_checksum_func_get_event_buffer_state_v2(void
+
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_GET_OFFICE_PREVIEW_CONFIG_FFI
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_GET_OFFICE_PREVIEW_CONFIG_FFI
uint16_t uniffi_zx_document_ffi_checksum_func_get_office_preview_config_ffi(void
+);
+#endif
+#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_GET_SESSION_V2_FFI
+#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_GET_SESSION_V2_FFI
+uint16_t uniffi_zx_document_ffi_checksum_func_get_session_v2_ffi(void
+
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_IS_OFFICE_TYPE_FFI
@@ -761,6 +842,18 @@ uint16_t uniffi_zx_document_ffi_checksum_func_is_office_type_ffi(void
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_MARK_EVENTS_FAILED_V2
uint16_t uniffi_zx_document_ffi_checksum_func_mark_events_failed_v2(void
+);
+#endif
+#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_MARK_SESSION_FAILED_V2
+#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_MARK_SESSION_FAILED_V2
+uint16_t uniffi_zx_document_ffi_checksum_func_mark_session_failed_v2(void
+
+);
+#endif
+#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_MARK_SESSION_INTERRUPTED_V2
+#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_MARK_SESSION_INTERRUPTED_V2
+uint16_t uniffi_zx_document_ffi_checksum_func_mark_session_interrupted_v2(void
+
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PARSE_MARKDOWN
@@ -779,6 +872,18 @@ uint16_t uniffi_zx_document_ffi_checksum_func_parse_text(void
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PAUSE_READING_SESSION_V2
uint16_t uniffi_zx_document_ffi_checksum_func_pause_reading_session_v2(void
+);
+#endif
+#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_CHECKPOINT_CHANGED_V2
+#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_CHECKPOINT_CHANGED_V2
+uint16_t uniffi_zx_document_ffi_checksum_func_push_checkpoint_changed_v2(void
+
+);
+#endif
+#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_HEARTBEAT_CHECKPOINT_V2
+#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_HEARTBEAT_CHECKPOINT_V2
+uint16_t uniffi_zx_document_ffi_checksum_func_push_heartbeat_checkpoint_v2(void
+
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_PUSH_HEARTBEAT_V2
@@ -887,6 +992,12 @@ uint16_t uniffi_zx_document_ffi_checksum_func_search_pdf_pages(void
#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SEARCH_TEXT_CONTENT
uint16_t uniffi_zx_document_ffi_checksum_func_search_text_content(void
+);
+#endif
+#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SET_SESSION_CLOSED_AT_MS_V2
+#define UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_SET_SESSION_CLOSED_AT_MS_V2
+uint16_t uniffi_zx_document_ffi_checksum_func_set_session_closed_at_ms_v2(void
+
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_ZX_DOCUMENT_FFI_CHECKSUM_FUNC_START_READING_SESSION_V2
diff --git a/bindings/ios/simulator/libzx_document_ffi.a b/bindings/ios/simulator/libzx_document_ffi.a
new file mode 100644
index 0000000..e1166df
Binary files /dev/null and b/bindings/ios/simulator/libzx_document_ffi.a differ
diff --git a/bindings/ios/simulator/libzx_document_ffi_arm64.a b/bindings/ios/simulator/libzx_document_ffi_arm64.a
new file mode 100644
index 0000000..8167a6f
Binary files /dev/null and b/bindings/ios/simulator/libzx_document_ffi_arm64.a differ
diff --git a/bindings/ios/simulator/libzx_document_ffi_x86_64.a b/bindings/ios/simulator/libzx_document_ffi_x86_64.a
new file mode 100644
index 0000000..39e7f1a
Binary files /dev/null and b/bindings/ios/simulator/libzx_document_ffi_x86_64.a differ
diff --git a/crates/zx_document_core/Cargo.toml b/crates/zx_document_core/Cargo.toml
index 921b231..bfe0131 100644
--- a/crates/zx_document_core/Cargo.toml
+++ b/crates/zx_document_core/Cargo.toml
@@ -13,6 +13,7 @@ uuid = { version = "1", features = ["v4"] }
zip = "2"
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif"] }
uniffi = "0.31"
+sha2 = "0.10"
[build-dependencies]
uniffi = { version = "0.31", features = ["build"] }
diff --git a/crates/zx_document_core/src/anchors.rs b/crates/zx_document_core/src/anchors.rs
index b336655..fa4471d 100644
--- a/crates/zx_document_core/src/anchors.rs
+++ b/crates/zx_document_core/src/anchors.rs
@@ -56,6 +56,13 @@ pub enum NoteAnchor {
impl NoteAnchor {
pub fn from_position(material_id: &str, position: Option<&ReadingPosition>) -> Self {
match position {
+ Some(pos @ ReadingPosition::Block { block_id, .. }) => {
+ NoteAnchor::MarkdownBlock {
+ material_id: material_id.to_string(),
+ block_id: block_id.clone(),
+ position_snapshot: Some(pos.clone()),
+ }
+ }
Some(pos @ ReadingPosition::Markdown { block_id, .. }) => {
NoteAnchor::MarkdownBlock {
material_id: material_id.to_string(),
@@ -77,7 +84,17 @@ impl NoteAnchor {
position_snapshot: Some(pos.clone()),
}
}
- Some(pos @ ReadingPosition::Image { .. }) => NoteAnchor::Image {
+ Some(pos @ ReadingPosition::PdfViewport { page_number, .. }) => {
+ NoteAnchor::PdfPage {
+ material_id: material_id.to_string(),
+ page_number: *page_number,
+ position_snapshot: Some(pos.clone()),
+ }
+ }
+ Some(pos @ ReadingPosition::Image { .. })
+ | Some(pos @ ReadingPosition::ImageViewport { .. })
+ | Some(pos @ ReadingPosition::Slide { .. })
+ | Some(pos @ ReadingPosition::Workbook { .. }) => NoteAnchor::Image {
material_id: material_id.to_string(),
position_snapshot: Some(pos.clone()),
},
diff --git a/crates/zx_document_core/src/events_v2.rs b/crates/zx_document_core/src/events_v2.rs
index 482af39..3bac48c 100644
--- a/crates/zx_document_core/src/events_v2.rs
+++ b/crates/zx_document_core/src/events_v2.rs
@@ -2,7 +2,7 @@ use std::sync::Mutex;
use serde::{Deserialize, Serialize};
-use crate::progress::ReadingPosition;
+use crate::progress::{ReadingCheckpoint, ReadingPosition};
use crate::session_v2;
const MAX_BUFFER_SIZE: usize = 1000;
@@ -50,6 +50,7 @@ pub struct ReadingEventV2 {
pub material_id: String,
pub event_type: ReadingEventTypeV2,
pub position: Option,
+ pub checkpoint: Option,
pub active_seconds_delta: u32,
pub timestamp_ms: i64,
pub sequence: u64,
@@ -62,14 +63,16 @@ fn push_event(
material_id: &str,
event_type: ReadingEventTypeV2,
position: Option,
+ checkpoint: Option,
active_seconds_delta: u32,
timestamp_ms: i64,
) -> Result {
- let seq = session_v2::record_session_event_v2(
+ let seq = session_v2::record_session_event_with_checkpoint_v2(
session_id,
timestamp_ms,
active_seconds_delta,
position.clone(),
+ checkpoint.clone(),
)?;
let event = ReadingEventV2 {
@@ -78,6 +81,7 @@ fn push_event(
material_id: material_id.to_string(),
event_type,
position: position.map(|p| p.normalized()),
+ checkpoint: checkpoint.map(|c| c.normalized()),
active_seconds_delta,
timestamp_ms,
sequence: seq,
@@ -115,7 +119,7 @@ pub fn push_material_opened_v2(
material_id: &str,
timestamp_ms: i64,
) -> Result {
- push_event(session_id, material_id, ReadingEventTypeV2::MaterialOpened, None, 0, timestamp_ms)
+ push_event(session_id, material_id, ReadingEventTypeV2::MaterialOpened, None, None, 0, timestamp_ms)
}
pub fn push_material_closed_v2(
@@ -124,7 +128,7 @@ pub fn push_material_closed_v2(
active_seconds_delta: u32,
timestamp_ms: i64,
) -> Result {
- push_event(session_id, material_id, ReadingEventTypeV2::MaterialClosed, None, active_seconds_delta, timestamp_ms)
+ push_event(session_id, material_id, ReadingEventTypeV2::MaterialClosed, None, None, active_seconds_delta, timestamp_ms)
}
pub fn push_position_changed_v2(
@@ -133,7 +137,24 @@ pub fn push_position_changed_v2(
position: ReadingPosition,
timestamp_ms: i64,
) -> Result {
- push_event(session_id, material_id, ReadingEventTypeV2::PositionChanged, Some(position), 0, timestamp_ms)
+ push_event(session_id, material_id, ReadingEventTypeV2::PositionChanged, Some(position), None, 0, timestamp_ms)
+}
+
+pub fn push_checkpoint_changed_v2(
+ session_id: &str,
+ material_id: &str,
+ checkpoint: ReadingCheckpoint,
+ timestamp_ms: i64,
+) -> Result {
+ push_event(
+ session_id,
+ material_id,
+ ReadingEventTypeV2::PositionChanged,
+ Some(checkpoint.legacy_position()),
+ Some(checkpoint),
+ 0,
+ timestamp_ms,
+ )
}
pub fn push_heartbeat_v2(
@@ -143,7 +164,26 @@ pub fn push_heartbeat_v2(
position: Option,
timestamp_ms: i64,
) -> Result {
- push_event(session_id, material_id, ReadingEventTypeV2::Heartbeat, position, active_seconds_delta, timestamp_ms)
+ push_event(session_id, material_id, ReadingEventTypeV2::Heartbeat, position, None, active_seconds_delta, timestamp_ms)
+}
+
+pub fn push_heartbeat_checkpoint_v2(
+ session_id: &str,
+ material_id: &str,
+ active_seconds_delta: u32,
+ checkpoint: Option,
+ timestamp_ms: i64,
+) -> Result {
+ let legacy_position = checkpoint.as_ref().map(ReadingCheckpoint::legacy_position);
+ push_event(
+ session_id,
+ material_id,
+ ReadingEventTypeV2::Heartbeat,
+ legacy_position,
+ checkpoint,
+ active_seconds_delta,
+ timestamp_ms,
+ )
}
pub fn push_marked_as_read_v2(
@@ -151,7 +191,7 @@ pub fn push_marked_as_read_v2(
material_id: &str,
timestamp_ms: i64,
) -> Result {
- push_event(session_id, material_id, ReadingEventTypeV2::MarkedAsRead, None, 0, timestamp_ms)
+ push_event(session_id, material_id, ReadingEventTypeV2::MarkedAsRead, None, None, 0, timestamp_ms)
}
// ── Buffer Management (V2 with ack) ──
diff --git a/crates/zx_document_core/src/fingerprint.rs b/crates/zx_document_core/src/fingerprint.rs
new file mode 100644
index 0000000..48afd7b
--- /dev/null
+++ b/crates/zx_document_core/src/fingerprint.rs
@@ -0,0 +1,54 @@
+use sha2::{Digest, Sha256};
+
+use crate::error::DocumentError;
+use crate::progress::RendererKind;
+
+pub const BLOCK_PARSER_VERSION: &str = "block-parser-v1";
+pub const WORKBOOK_PARSER_VERSION: &str = "workbook-parser-v1";
+pub const SLIDE_PARSER_VERSION: &str = "slide-parser-v1";
+pub const PDF_LOCATOR_SCHEMA_VERSION: u32 = 1;
+pub const IMAGE_LOCATOR_SCHEMA_VERSION: u32 = 1;
+
+pub fn compute_file_fingerprint(file_path: &str) -> Result {
+ let bytes = std::fs::read(file_path)?;
+ Ok(hex_sha256(&bytes))
+}
+
+pub fn build_representation_fingerprint(
+ source_fingerprint: &str,
+ renderer_kind: &RendererKind,
+ parser_version: &str,
+ schema_version: u32,
+) -> String {
+ let payload = format!(
+ "{source_fingerprint}:{renderer_kind}:{parser_version}:schema-v{schema_version}"
+ );
+ hex_sha256(payload.as_bytes())
+}
+
+fn hex_sha256(bytes: &[u8]) -> String {
+ let mut hasher = Sha256::new();
+ hasher.update(bytes);
+ format!("{:x}", hasher.finalize())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn representation_fingerprint_is_stable() {
+ let a = build_representation_fingerprint("abc", &RendererKind::Block, BLOCK_PARSER_VERSION, 2);
+ let b = build_representation_fingerprint("abc", &RendererKind::Block, BLOCK_PARSER_VERSION, 2);
+ assert_eq!(a, b);
+ }
+
+ #[test]
+ fn representation_fingerprint_changes_on_inputs() {
+ let base = build_representation_fingerprint("abc", &RendererKind::Block, BLOCK_PARSER_VERSION, 2);
+ assert_ne!(base, build_representation_fingerprint("def", &RendererKind::Block, BLOCK_PARSER_VERSION, 2));
+ assert_ne!(base, build_representation_fingerprint("abc", &RendererKind::Pdf, BLOCK_PARSER_VERSION, 2));
+ assert_ne!(base, build_representation_fingerprint("abc", &RendererKind::Block, "block-parser-v2", 2));
+ assert_ne!(base, build_representation_fingerprint("abc", &RendererKind::Block, BLOCK_PARSER_VERSION, 3));
+ }
+}
diff --git a/crates/zx_document_core/src/lib.rs b/crates/zx_document_core/src/lib.rs
index 7d016d7..8c533b9 100644
--- a/crates/zx_document_core/src/lib.rs
+++ b/crates/zx_document_core/src/lib.rs
@@ -7,6 +7,7 @@ pub mod epub;
pub mod error;
pub mod events;
pub mod events_v2;
+pub mod fingerprint;
pub mod image_meta;
pub mod markdown;
pub mod material_type;
@@ -16,5 +17,6 @@ pub mod progress;
pub mod reading_material;
pub mod search;
pub mod session_v2;
+pub mod stable_id;
pub mod text;
pub mod time_tracker;
diff --git a/crates/zx_document_core/src/markdown.rs b/crates/zx_document_core/src/markdown.rs
index 07f60d9..467560b 100644
--- a/crates/zx_document_core/src/markdown.rs
+++ b/crates/zx_document_core/src/markdown.rs
@@ -1,12 +1,19 @@
use comrak::nodes::{AstNode, NodeValue};
use comrak::{Arena, ComrakOptions};
-use uuid::Uuid;
use crate::blocks::DocumentBlock;
use crate::error::DocumentError;
+use crate::stable_id::stable_content_id;
-fn block_id() -> String {
- Uuid::new_v4().to_string()
+/// Generate a stable, deterministic block ID from the block's text content
+/// and its zero-based position within the document.
+///
+/// Uses djb2 — a simple, fixed-algorithm hash — so the same `(text, position)`
+/// pair always produces the same hex string **across app launches / processes**.
+/// This is critical for reading-position restore: the saved `lastPosition.blockId`
+/// must match the id of the same block when the document is re-parsed later.
+fn block_id(text: &str, position: usize) -> String {
+ stable_content_id("markdown", position, text)
}
fn gfm_options() -> ComrakOptions<'static> {
@@ -20,12 +27,14 @@ fn gfm_options() -> ComrakOptions<'static> {
/// Parse a Markdown string into a list of DocumentBlock.
pub fn parse_markdown(md_content: &str) -> Result, DocumentError> {
+ let start = std::time::Instant::now();
let arena = Arena::new();
let options = gfm_options();
let root = comrak::parse_document(&arena, md_content, &options);
let mut blocks = Vec::new();
collect_blocks(root, &mut blocks);
+ eprintln!("[Rust] parse_markdown — len={} block_count={} elapsed_ms={}", md_content.len(), blocks.len(), start.elapsed().as_millis());
Ok(blocks)
}
@@ -35,14 +44,17 @@ fn collect_blocks<'a>(node: &'a AstNode<'a>, blocks: &mut Vec) {
let value = &data.value;
let block = match value {
- NodeValue::Heading(heading) => Some(DocumentBlock::Heading {
- id: block_id(),
- level: heading.level,
- text: collect_text(child),
- }),
+ NodeValue::Heading(heading) => {
+ let text = collect_text(child);
+ Some(DocumentBlock::Heading {
+ id: block_id(&text, blocks.len()),
+ level: heading.level,
+ text,
+ })
+ }
NodeValue::Paragraph => {
// Check for inline image — extract as standalone Image block
- if let Some(image_block) = extract_image_from_paragraph(child) {
+ if let Some(image_block) = extract_image_from_paragraph(child, blocks.len()) {
Some(image_block)
} else {
let text = collect_text(child);
@@ -50,7 +62,7 @@ fn collect_blocks<'a>(node: &'a AstNode<'a>, blocks: &mut Vec) {
None
} else {
Some(DocumentBlock::Paragraph {
- id: block_id(),
+ id: block_id(&text, blocks.len()),
text,
})
}
@@ -71,14 +83,14 @@ fn collect_blocks<'a>(node: &'a AstNode<'a>, blocks: &mut Vec) {
None
} else {
Some(DocumentBlock::List {
- id: block_id(),
+ id: block_id(&items.join("\n"), blocks.len()),
ordered: list.list_type == comrak::nodes::ListType::Ordered,
items,
})
}
}
NodeValue::CodeBlock(code) => Some(DocumentBlock::CodeBlock {
- id: block_id(),
+ id: block_id(&code.literal, blocks.len()),
language: if code.info.is_empty() {
None
} else {
@@ -92,7 +104,7 @@ fn collect_blocks<'a>(node: &'a AstNode<'a>, blocks: &mut Vec) {
None
} else {
Some(DocumentBlock::Quote {
- id: block_id(),
+ id: block_id(&text, blocks.len()),
text,
})
}
@@ -124,7 +136,7 @@ fn collect_blocks<'a>(node: &'a AstNode<'a>, blocks: &mut Vec) {
None
} else {
Some(DocumentBlock::Table {
- id: block_id(),
+ id: block_id(&headers.join("|"), blocks.len()),
headers,
rows,
})
@@ -133,7 +145,7 @@ fn collect_blocks<'a>(node: &'a AstNode<'a>, blocks: &mut Vec) {
NodeValue::Image(image) => {
let alt_text = collect_text(child);
Some(DocumentBlock::Image {
- id: block_id(),
+ id: block_id(&image.url, blocks.len()),
src: image.url.clone(),
alt: if alt_text.is_empty() {
None
@@ -143,7 +155,7 @@ fn collect_blocks<'a>(node: &'a AstNode<'a>, blocks: &mut Vec) {
})
}
NodeValue::ThematicBreak => Some(DocumentBlock::HorizontalRule {
- id: block_id(),
+ id: block_id("---", blocks.len()),
}),
// Recurse into containers: Document, Item, TableCell, TableRow, etc.
NodeValue::HtmlBlock(block) => {
@@ -152,7 +164,7 @@ fn collect_blocks<'a>(node: &'a AstNode<'a>, blocks: &mut Vec) {
None
} else {
Some(DocumentBlock::CodeBlock {
- id: block_id(),
+ id: block_id(&html, blocks.len()),
language: Some("html".into()),
code: html,
})
@@ -163,7 +175,7 @@ fn collect_blocks<'a>(node: &'a AstNode<'a>, blocks: &mut Vec) {
None
} else {
Some(DocumentBlock::CodeBlock {
- id: block_id(),
+ id: block_id(&content, blocks.len()),
language: Some("yaml".into()),
code: content.clone(),
})
@@ -174,7 +186,7 @@ fn collect_blocks<'a>(node: &'a AstNode<'a>, blocks: &mut Vec) {
if text.is_empty() {
None
} else {
- Some(DocumentBlock::Paragraph { id: block_id(), text })
+ Some(DocumentBlock::Paragraph { id: block_id(&text, blocks.len()), text })
}
}
NodeValue::Document
@@ -198,7 +210,7 @@ fn collect_blocks<'a>(node: &'a AstNode<'a>, blocks: &mut Vec) {
}
/// Check if a paragraph contains only an image, and extract it.
-fn extract_image_from_paragraph<'a>(node: &'a AstNode<'a>) -> Option {
+fn extract_image_from_paragraph<'a>(node: &'a AstNode<'a>, position: usize) -> Option {
let mut image_node: Option<&AstNode> = None;
let mut has_other_content = false;
@@ -222,7 +234,7 @@ fn extract_image_from_paragraph<'a>(node: &'a AstNode<'a>) -> Option(node: &'a AstNode<'a>, buf: &mut String) {
mod tests {
use super::*;
+ fn block_id_str(b: &DocumentBlock) -> &str {
+ match b {
+ DocumentBlock::Heading { id, .. } => id,
+ DocumentBlock::Paragraph { id, .. } => id,
+ DocumentBlock::List { id, .. } => id,
+ DocumentBlock::CodeBlock { id, .. } => id,
+ DocumentBlock::Quote { id, .. } => id,
+ DocumentBlock::Table { id, .. } => id,
+ DocumentBlock::Image { id, .. } => id,
+ DocumentBlock::HorizontalRule { id, .. } => id,
+ }
+ }
+
#[test]
fn test_empty_markdown() {
let blocks = parse_markdown("").unwrap();
@@ -516,6 +541,20 @@ mod tests {
assert!(blocks.iter().any(|b| matches!(b, DocumentBlock::List { .. })));
}
+ #[test]
+ fn test_block_ids_are_deterministic_across_parses() {
+ let md = "# Title\n\nHello world.\n\n- item 1\n- item 2\n\n```rust\nfn main() {}\n```\n\n> Quote";
+ let blocks1 = parse_markdown(md).unwrap();
+ let blocks2 = parse_markdown(md).unwrap();
+
+ assert_eq!(blocks1.len(), blocks2.len());
+ for (a, b) in blocks1.iter().zip(blocks2.iter()) {
+ let id_a = block_id_str(a);
+ let id_b = block_id_str(b);
+ assert_eq!(id_a, id_b, "block_id mismatch: {:?} vs {:?}", a, b);
+ }
+ }
+
#[test]
fn test_all_eight_block_types() {
let md = "# H\n\nPara.\n\n- item\n\n```rs\nlet x=1;\n```\n\n> Quote\n\n|A|B|\n|---|---|\n|1|2|\n\n\n\n---";
diff --git a/crates/zx_document_core/src/progress.rs b/crates/zx_document_core/src/progress.rs
index d182c00..b2b4907 100644
--- a/crates/zx_document_core/src/progress.rs
+++ b/crates/zx_document_core/src/progress.rs
@@ -2,14 +2,52 @@ use serde::{Deserialize, Serialize, Serializer};
/// Clamp a progress value to 0..1. NaN→0, <0→0, >1→1.
pub fn clamp_progress(v: f32) -> f32 {
- if v.is_nan() || v < 0.0 { return 0.0; }
- if v > 1.0 { return 1.0; }
+ if v.is_nan() || v < 0.0 {
+ return 0.0;
+ }
+ if v > 1.0 {
+ return 1.0;
+ }
v
}
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, uniffi::Enum)]
+#[serde(rename_all = "camelCase")]
+pub enum RendererKind {
+ Block,
+ Pdf,
+ Image,
+ Workbook,
+ Slide,
+ Epub,
+ Unknown,
+}
+
+impl std::fmt::Display for RendererKind {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::Block => write!(f, "block"),
+ Self::Pdf => write!(f, "pdf"),
+ Self::Image => write!(f, "image"),
+ Self::Workbook => write!(f, "workbook"),
+ Self::Slide => write!(f, "slide"),
+ Self::Epub => write!(f, "epub"),
+ Self::Unknown => write!(f, "unknown"),
+ }
+ }
+}
+
#[derive(Debug, Clone, PartialEq, Deserialize, uniffi::Enum)]
#[serde(tag = "type")]
pub enum ReadingPosition {
+ Block {
+ #[serde(rename = "blockId")]
+ block_id: String,
+ #[serde(rename = "blockProgress")]
+ block_progress: f32,
+ #[serde(rename = "overallProgress")]
+ overall_progress: f32,
+ },
Markdown {
#[serde(rename = "blockId")]
block_id: String,
@@ -30,6 +68,18 @@ pub enum ReadingPosition {
#[serde(rename = "overallProgress")]
overall_progress: f32,
},
+ PdfViewport {
+ #[serde(rename = "pageNumber")]
+ page_number: u32,
+ #[serde(rename = "anchorX")]
+ anchor_x: f32,
+ #[serde(rename = "anchorY")]
+ anchor_y: f32,
+ #[serde(rename = "zoomScale")]
+ zoom_scale: f32,
+ #[serde(rename = "overallProgress")]
+ overall_progress: f32,
+ },
Image {
#[serde(rename = "zoomScale")]
zoom_scale: f32,
@@ -38,6 +88,40 @@ pub enum ReadingPosition {
#[serde(rename = "offsetY")]
offset_y: f32,
},
+ ImageViewport {
+ #[serde(rename = "zoomScale")]
+ zoom_scale: f32,
+ #[serde(rename = "anchorX")]
+ anchor_x: f32,
+ #[serde(rename = "anchorY")]
+ anchor_y: f32,
+ },
+ Workbook {
+ #[serde(rename = "sheetId")]
+ sheet_id: String,
+ #[serde(rename = "topRow")]
+ top_row: u32,
+ #[serde(rename = "leftColumn")]
+ left_column: u32,
+ #[serde(rename = "selectedCell")]
+ selected_cell: Option,
+ #[serde(rename = "zoomScale")]
+ zoom_scale: f32,
+ #[serde(rename = "overallProgress")]
+ overall_progress: Option,
+ },
+ Slide {
+ #[serde(rename = "slideIndex")]
+ slide_index: u32,
+ #[serde(rename = "zoomScale")]
+ zoom_scale: f32,
+ #[serde(rename = "anchorX")]
+ anchor_x: f32,
+ #[serde(rename = "anchorY")]
+ anchor_y: f32,
+ #[serde(rename = "overallProgress")]
+ overall_progress: f32,
+ },
Epub {
#[serde(rename = "chapterId")]
chapter_id: String,
@@ -49,47 +133,127 @@ pub enum ReadingPosition {
Unknown,
}
-// Manual Serialize to enforce camelCase field names + clamped progress values.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, uniffi::Record)]
+#[serde(rename_all = "camelCase")]
+pub struct ReadingCheckpoint {
+ pub schema_version: u32,
+ pub material_id: String,
+ pub representation_fingerprint: String,
+ pub renderer_kind: RendererKind,
+ pub captured_at_ms: u64,
+ pub device_id: String,
+ pub device_sequence: u64,
+ pub position: ReadingPosition,
+}
+
+impl ReadingCheckpoint {
+ pub fn normalized(&self) -> Self {
+ Self {
+ schema_version: self.schema_version,
+ material_id: self.material_id.clone(),
+ representation_fingerprint: self.representation_fingerprint.clone(),
+ renderer_kind: self.renderer_kind.clone(),
+ captured_at_ms: self.captured_at_ms,
+ device_id: self.device_id.clone(),
+ device_sequence: self.device_sequence,
+ position: self.position.normalized(),
+ }
+ }
+
+ pub fn legacy_position(&self) -> ReadingPosition {
+ self.position.clone()
+ }
+}
+
+// Manual Serialize to enforce camelCase field names + normalized values.
impl Serialize for ReadingPosition {
fn serialize(&self, s: S) -> Result {
use serde::ser::SerializeStruct;
- match self {
+
+ match self.normalized() {
+ Self::Block { block_id, block_progress, overall_progress } => {
+ let mut st = s.serialize_struct("Block", 4)?;
+ st.serialize_field("type", "Block")?;
+ st.serialize_field("blockId", &block_id)?;
+ st.serialize_field("blockProgress", &block_progress)?;
+ st.serialize_field("overallProgress", &overall_progress)?;
+ st.end()
+ }
Self::Markdown { block_id, scroll_progress } => {
let mut st = s.serialize_struct("Markdown", 3)?;
st.serialize_field("type", "Markdown")?;
- st.serialize_field("blockId", block_id)?;
- st.serialize_field("scrollProgress", &clamp_progress(*scroll_progress))?;
+ st.serialize_field("blockId", &block_id)?;
+ st.serialize_field("scrollProgress", &scroll_progress)?;
st.end()
}
Self::Text { line_number, scroll_progress } => {
let mut st = s.serialize_struct("Text", 3)?;
st.serialize_field("type", "Text")?;
- st.serialize_field("lineNumber", line_number)?;
- st.serialize_field("scrollProgress", &clamp_progress(*scroll_progress))?;
+ st.serialize_field("lineNumber", &line_number)?;
+ st.serialize_field("scrollProgress", &scroll_progress)?;
st.end()
}
Self::Pdf { page_number, page_progress, overall_progress } => {
let mut st = s.serialize_struct("Pdf", 4)?;
st.serialize_field("type", "Pdf")?;
- st.serialize_field("pageNumber", page_number)?;
- st.serialize_field("pageProgress", &clamp_progress(*page_progress))?;
- st.serialize_field("overallProgress", &clamp_progress(*overall_progress))?;
+ st.serialize_field("pageNumber", &page_number)?;
+ st.serialize_field("pageProgress", &page_progress)?;
+ st.serialize_field("overallProgress", &overall_progress)?;
+ st.end()
+ }
+ Self::PdfViewport { page_number, anchor_x, anchor_y, zoom_scale, overall_progress } => {
+ let mut st = s.serialize_struct("PdfViewport", 6)?;
+ st.serialize_field("type", "PdfViewport")?;
+ st.serialize_field("pageNumber", &page_number)?;
+ st.serialize_field("anchorX", &anchor_x)?;
+ st.serialize_field("anchorY", &anchor_y)?;
+ st.serialize_field("zoomScale", &zoom_scale)?;
+ st.serialize_field("overallProgress", &overall_progress)?;
st.end()
}
Self::Image { zoom_scale, offset_x, offset_y } => {
let mut st = s.serialize_struct("Image", 4)?;
st.serialize_field("type", "Image")?;
- st.serialize_field("zoomScale", zoom_scale)?;
- st.serialize_field("offsetX", offset_x)?;
- st.serialize_field("offsetY", offset_y)?;
+ st.serialize_field("zoomScale", &zoom_scale)?;
+ st.serialize_field("offsetX", &offset_x)?;
+ st.serialize_field("offsetY", &offset_y)?;
+ st.end()
+ }
+ Self::ImageViewport { zoom_scale, anchor_x, anchor_y } => {
+ let mut st = s.serialize_struct("ImageViewport", 4)?;
+ st.serialize_field("type", "ImageViewport")?;
+ st.serialize_field("zoomScale", &zoom_scale)?;
+ st.serialize_field("anchorX", &anchor_x)?;
+ st.serialize_field("anchorY", &anchor_y)?;
+ st.end()
+ }
+ Self::Workbook { sheet_id, top_row, left_column, selected_cell, zoom_scale, overall_progress } => {
+ let mut st = s.serialize_struct("Workbook", 7)?;
+ st.serialize_field("type", "Workbook")?;
+ st.serialize_field("sheetId", &sheet_id)?;
+ st.serialize_field("topRow", &top_row)?;
+ st.serialize_field("leftColumn", &left_column)?;
+ st.serialize_field("selectedCell", &selected_cell)?;
+ st.serialize_field("zoomScale", &zoom_scale)?;
+ st.serialize_field("overallProgress", &overall_progress)?;
+ st.end()
+ }
+ Self::Slide { slide_index, zoom_scale, anchor_x, anchor_y, overall_progress } => {
+ let mut st = s.serialize_struct("Slide", 6)?;
+ st.serialize_field("type", "Slide")?;
+ st.serialize_field("slideIndex", &slide_index)?;
+ st.serialize_field("zoomScale", &zoom_scale)?;
+ st.serialize_field("anchorX", &anchor_x)?;
+ st.serialize_field("anchorY", &anchor_y)?;
+ st.serialize_field("overallProgress", &overall_progress)?;
st.end()
}
Self::Epub { chapter_id, chapter_progress, overall_progress } => {
let mut st = s.serialize_struct("Epub", 4)?;
st.serialize_field("type", "Epub")?;
- st.serialize_field("chapterId", chapter_id)?;
- st.serialize_field("chapterProgress", &clamp_progress(*chapter_progress))?;
- st.serialize_field("overallProgress", &clamp_progress(*overall_progress))?;
+ st.serialize_field("chapterId", &chapter_id)?;
+ st.serialize_field("chapterProgress", &chapter_progress)?;
+ st.serialize_field("overallProgress", &overall_progress)?;
st.end()
}
Self::Unknown => {
@@ -105,6 +269,11 @@ impl ReadingPosition {
/// Return a normalized copy with all progress fields clamped to 0..1.
pub fn normalized(&self) -> Self {
match self {
+ Self::Block { block_id, block_progress, overall_progress } => Self::Block {
+ block_id: block_id.clone(),
+ block_progress: clamp_progress(*block_progress),
+ overall_progress: clamp_progress(*overall_progress),
+ },
Self::Markdown { block_id, scroll_progress } => Self::Markdown {
block_id: block_id.clone(),
scroll_progress: clamp_progress(*scroll_progress),
@@ -118,8 +287,37 @@ impl ReadingPosition {
page_progress: clamp_progress(*page_progress),
overall_progress: clamp_progress(*overall_progress),
},
+ Self::PdfViewport { page_number, anchor_x, anchor_y, zoom_scale, overall_progress } => Self::PdfViewport {
+ page_number: *page_number,
+ anchor_x: clamp_progress(*anchor_x),
+ anchor_y: clamp_progress(*anchor_y),
+ zoom_scale: *zoom_scale,
+ overall_progress: clamp_progress(*overall_progress),
+ },
Self::Image { zoom_scale, offset_x, offset_y } => Self::Image {
- zoom_scale: *zoom_scale, offset_x: *offset_x, offset_y: *offset_y,
+ zoom_scale: *zoom_scale,
+ offset_x: *offset_x,
+ offset_y: *offset_y,
+ },
+ Self::ImageViewport { zoom_scale, anchor_x, anchor_y } => Self::ImageViewport {
+ zoom_scale: *zoom_scale,
+ anchor_x: clamp_progress(*anchor_x),
+ anchor_y: clamp_progress(*anchor_y),
+ },
+ Self::Workbook { sheet_id, top_row, left_column, selected_cell, zoom_scale, overall_progress } => Self::Workbook {
+ sheet_id: sheet_id.clone(),
+ top_row: *top_row,
+ left_column: *left_column,
+ selected_cell: selected_cell.clone(),
+ zoom_scale: *zoom_scale,
+ overall_progress: overall_progress.map(clamp_progress),
+ },
+ Self::Slide { slide_index, zoom_scale, anchor_x, anchor_y, overall_progress } => Self::Slide {
+ slide_index: *slide_index,
+ zoom_scale: *zoom_scale,
+ anchor_x: clamp_progress(*anchor_x),
+ anchor_y: clamp_progress(*anchor_y),
+ overall_progress: clamp_progress(*overall_progress),
},
Self::Epub { chapter_id, chapter_progress, overall_progress } => Self::Epub {
chapter_id: chapter_id.clone(),
@@ -135,10 +333,17 @@ impl ReadingPosition {
use crate::material_type::MaterialType;
matches!(
(self, mt),
- (ReadingPosition::Markdown { .. }, MaterialType::Markdown)
+ (ReadingPosition::Block { .. }, MaterialType::Markdown)
+ | (ReadingPosition::Block { .. }, MaterialType::Text)
+ | (ReadingPosition::Block { .. }, MaterialType::Word)
+ | (ReadingPosition::Markdown { .. }, MaterialType::Markdown)
| (ReadingPosition::Text { .. }, MaterialType::Text)
| (ReadingPosition::Pdf { .. }, MaterialType::Pdf)
+ | (ReadingPosition::PdfViewport { .. }, MaterialType::Pdf)
| (ReadingPosition::Image { .. }, MaterialType::Image)
+ | (ReadingPosition::ImageViewport { .. }, MaterialType::Image)
+ | (ReadingPosition::Workbook { .. }, MaterialType::Excel)
+ | (ReadingPosition::Slide { .. }, MaterialType::PowerPoint)
| (ReadingPosition::Epub { .. }, MaterialType::Epub)
| (ReadingPosition::Unknown, _)
)
@@ -146,11 +351,15 @@ impl ReadingPosition {
pub fn progress_value(&self) -> Option {
match self {
+ Self::Block { overall_progress, .. } => Some(clamp_progress(*overall_progress)),
Self::Markdown { scroll_progress, .. } => Some(clamp_progress(*scroll_progress)),
Self::Text { scroll_progress, .. } => Some(clamp_progress(*scroll_progress)),
Self::Pdf { overall_progress, .. } => Some(clamp_progress(*overall_progress)),
+ Self::PdfViewport { overall_progress, .. } => Some(clamp_progress(*overall_progress)),
+ Self::Workbook { overall_progress, .. } => overall_progress.map(clamp_progress),
+ Self::Slide { overall_progress, .. } => Some(clamp_progress(*overall_progress)),
Self::Epub { overall_progress, .. } => Some(clamp_progress(*overall_progress)),
- Self::Image { .. } | Self::Unknown => None,
+ Self::Image { .. } | Self::ImageViewport { .. } | Self::Unknown => None,
}
}
}
@@ -160,154 +369,89 @@ mod tests {
use super::*;
#[test]
- fn test_markdown_camel_case() {
+ fn test_block_roundtrip() {
+ let pos = ReadingPosition::Block {
+ block_id: "b1".into(),
+ block_progress: 0.2,
+ overall_progress: 0.4,
+ };
+ let json = serde_json::to_string(&pos).unwrap();
+ assert!(json.contains("\"type\":\"Block\""));
+ assert!(json.contains("\"blockId\":\"b1\""));
+ let back: ReadingPosition = serde_json::from_str(&json).unwrap();
+ assert_eq!(back, pos);
+ }
+
+ #[test]
+ fn test_pdf_viewport_clamps_anchor_and_progress() {
+ let pos = ReadingPosition::PdfViewport {
+ page_number: 1,
+ anchor_x: 2.0,
+ anchor_y: -1.0,
+ zoom_scale: 1.5,
+ overall_progress: f32::NAN,
+ };
+ let normalized = pos.normalized();
+ assert_eq!(
+ normalized,
+ ReadingPosition::PdfViewport {
+ page_number: 1,
+ anchor_x: 1.0,
+ anchor_y: 0.0,
+ zoom_scale: 1.5,
+ overall_progress: 0.0,
+ }
+ );
+ }
+
+ #[test]
+ fn test_legacy_markdown_camel_case() {
let pos = ReadingPosition::Markdown { block_id: "h1".into(), scroll_progress: 0.5 };
let json = serde_json::to_string(&pos).unwrap();
- eprintln!("SERIALIZED: {json}");
- assert!(json.contains("\"type\":\"Markdown\""), "missing type: {json}");
- assert!(json.contains("\"blockId\":\"h1\""), "missing blockId: {json}");
- assert!(json.contains("\"scrollProgress\":0.5"), "missing progress: {json}");
+ assert!(json.contains("\"blockId\":\"h1\""));
+ assert!(json.contains("\"scrollProgress\":0.5"));
}
#[test]
- fn test_pdf_camel_case() {
- let pos = ReadingPosition::Pdf { page_number: 7, page_progress: 0.8, overall_progress: 0.35 };
- let json = serde_json::to_string(&pos).unwrap();
- assert!(json.contains("\"pageNumber\":7"));
- assert!(json.contains("\"pageProgress\":0.8"));
- assert!(json.contains("\"overallProgress\":0.35"));
+ fn test_checkpoint_roundtrip() {
+ let checkpoint = ReadingCheckpoint {
+ schema_version: 2,
+ material_id: "mat-1".into(),
+ representation_fingerprint: "fp-1".into(),
+ renderer_kind: RendererKind::Block,
+ captured_at_ms: 100,
+ device_id: "device-1".into(),
+ device_sequence: 7,
+ position: ReadingPosition::Block {
+ block_id: "b1".into(),
+ block_progress: 0.25,
+ overall_progress: 0.5,
+ },
+ };
+ let json = serde_json::to_string(&checkpoint).unwrap();
+ assert!(json.contains("\"rendererKind\":\"block\""));
+ let back: ReadingCheckpoint = serde_json::from_str(&json).unwrap();
+ assert_eq!(back, checkpoint);
}
#[test]
- fn test_clamp_nan() {
- let pos = ReadingPosition::Markdown { block_id: "h1".into(), scroll_progress: f32::NAN };
- let json = serde_json::to_string(&pos).unwrap();
- assert!(json.contains("\"scrollProgress\":0.0"));
- }
-
- #[test]
- fn test_clamp_negative() {
- let pos = ReadingPosition::Text { line_number: 1, scroll_progress: -0.5 };
- let json = serde_json::to_string(&pos).unwrap();
- assert!(json.contains("\"scrollProgress\":0.0"));
- }
-
- #[test]
- fn test_clamp_above_one() {
- let pos = ReadingPosition::Pdf { page_number: 1, page_progress: 2.5, overall_progress: 10.0 };
- let json = serde_json::to_string(&pos).unwrap();
- assert!(json.contains("\"pageProgress\":1.0"));
- assert!(json.contains("\"overallProgress\":1.0"));
- }
-
- #[test]
- fn test_is_compatible() {
- use crate::material_type::MaterialType;
- let md = ReadingPosition::Markdown { block_id: "h1".into(), scroll_progress: 0.5 };
- assert!(md.is_compatible_with(&MaterialType::Markdown));
- assert!(!md.is_compatible_with(&MaterialType::Pdf));
- assert!(ReadingPosition::Unknown.is_compatible_with(&MaterialType::Pdf));
- assert!(ReadingPosition::Unknown.is_compatible_with(&MaterialType::Markdown)); // Unknown = compatible with all
- }
-
- #[test]
- fn test_progress_value() {
- assert_eq!(ReadingPosition::Unknown.progress_value(), None);
- let md = ReadingPosition::Markdown { block_id: "h1".into(), scroll_progress: 0.75 };
- assert_eq!(md.progress_value(), Some(0.75));
- }
-
- #[test]
- fn test_roundtrip() {
- let positions = vec![
- ReadingPosition::Markdown { block_id: "h1".into(), scroll_progress: 0.5 },
- ReadingPosition::Pdf { page_number: 7, page_progress: 0.8, overall_progress: 0.35 },
- ReadingPosition::Unknown,
- ];
- for pos in &positions {
- let json = serde_json::to_string(pos).unwrap();
- eprintln!("ROUNDTRIP JSON: {json}");
- let back: ReadingPosition = serde_json::from_str(&json).unwrap();
- assert_eq!(&back, pos);
- }
- }
-
- #[test]
- fn test_deserialize_from_camel_case_json() {
- // Simulate JSON from iOS app / API server: all camelCase field names
- let json = r#"{"type":"Markdown","blockId":"h1","scrollProgress":0.5}"#;
- let pos: ReadingPosition = serde_json::from_str(json).unwrap();
+ fn test_deserialize_legacy_and_new_json() {
+ let md_json = r#"{"type":"Markdown","blockId":"h1","scrollProgress":0.5}"#;
+ let md: ReadingPosition = serde_json::from_str(md_json).unwrap();
assert_eq!(
- pos,
+ md,
ReadingPosition::Markdown { block_id: "h1".into(), scroll_progress: 0.5 }
);
- let json = r#"{"type":"Text","lineNumber":5,"scrollProgress":0.3}"#;
- let pos: ReadingPosition = serde_json::from_str(json).unwrap();
+ let block_json = r#"{"type":"Block","blockId":"b1","blockProgress":0.3,"overallProgress":0.6}"#;
+ let block: ReadingPosition = serde_json::from_str(block_json).unwrap();
assert_eq!(
- pos,
- ReadingPosition::Text { line_number: 5, scroll_progress: 0.3 }
+ block,
+ ReadingPosition::Block {
+ block_id: "b1".into(),
+ block_progress: 0.3,
+ overall_progress: 0.6,
+ }
);
-
- let json = r#"{"type":"Pdf","pageNumber":3,"pageProgress":0.8,"overallProgress":0.35}"#;
- let pos: ReadingPosition = serde_json::from_str(json).unwrap();
- assert_eq!(
- pos,
- ReadingPosition::Pdf { page_number: 3, page_progress: 0.8, overall_progress: 0.35 }
- );
-
- let json = r#"{"type":"Image","zoomScale":1.5,"offsetX":10.0,"offsetY":20.0}"#;
- let pos: ReadingPosition = serde_json::from_str(json).unwrap();
- assert_eq!(
- pos,
- ReadingPosition::Image { zoom_scale: 1.5, offset_x: 10.0, offset_y: 20.0 }
- );
-
- let json = r#"{"type":"Epub","chapterId":"ch3","chapterProgress":0.6,"overallProgress":0.3}"#;
- let pos: ReadingPosition = serde_json::from_str(json).unwrap();
- assert_eq!(
- pos,
- ReadingPosition::Epub { chapter_id: "ch3".into(), chapter_progress: 0.6, overall_progress: 0.3 }
- );
-
- let json = r#"{"type":"Unknown"}"#;
- let pos: ReadingPosition = serde_json::from_str(json).unwrap();
- assert_eq!(pos, ReadingPosition::Unknown);
- }
-
- #[test]
- fn test_deserialize_serialize_roundtrip_all_variants() {
- // Deserialize from camelCase → re-serialize → deserialize again → verify identical
- let positions = vec![
- r#"{"type":"Markdown","blockId":"intro","scrollProgress":0.25}"#,
- r#"{"type":"Text","lineNumber":42,"scrollProgress":0.5}"#,
- r#"{"type":"Pdf","pageNumber":7,"pageProgress":0.8,"overallProgress":0.35}"#,
- r#"{"type":"Image","zoomScale":2.0,"offsetX":0.0,"offsetY":100.0}"#,
- r#"{"type":"Epub","chapterId":"ch1","chapterProgress":0.6,"overallProgress":0.3}"#,
- r#"{"type":"Unknown"}"#,
- ];
- for json_str in &positions {
- let pos1: ReadingPosition = serde_json::from_str(json_str).unwrap();
- let re_serialized = serde_json::to_string(&pos1).unwrap();
- let pos2: ReadingPosition = serde_json::from_str(&re_serialized).unwrap();
- assert_eq!(pos1, pos2, "roundtrip failed for: {json_str}");
- }
- }
-
- #[test]
- fn test_rename_attrs_not_overridden_by_uniffi() {
- // Verify #[serde(rename)] works correctly alongside #[derive(uniffi::Enum)]:
- // serde uses renamed camelCase names, not Rust snake_case names.
- // If uniffi overrode the renames, "blockId" would NOT be recognized.
-
- // 1. snake_case should FAIL (field is renamed)
- let json_snake = r#"{"type":"Markdown","block_id":"h1","scroll_progress":0.5}"#;
- let result: Result = serde_json::from_str(json_snake);
- assert!(result.is_err(), "snake_case should fail: rename=blockId means block_id is unknown");
-
- // 2. camelCase should SUCCEED
- let json_camel = r#"{"type":"Markdown","blockId":"h1","scrollProgress":0.5}"#;
- let result: Result = serde_json::from_str(json_camel);
- assert!(result.is_ok(), "camelCase should succeed: rename=blockId is the expected name");
}
}
diff --git a/crates/zx_document_core/src/session_v2.rs b/crates/zx_document_core/src/session_v2.rs
index 46d854e..43bccae 100644
--- a/crates/zx_document_core/src/session_v2.rs
+++ b/crates/zx_document_core/src/session_v2.rs
@@ -1,7 +1,7 @@
use std::collections::HashMap;
use std::sync::Mutex;
-use crate::progress::ReadingPosition;
+use crate::progress::{ReadingCheckpoint, ReadingPosition};
use crate::reading_material::ReadingMaterialRefV2;
use crate::time_tracker::ActiveTimeTracker;
@@ -36,6 +36,7 @@ pub struct ReadingSessionV2 {
pub next_sequence: u64,
pub total_active_seconds: u32,
pub last_position: Option,
+ pub last_checkpoint: Option,
pub status: ReadingSessionStatus,
}
@@ -58,6 +59,7 @@ pub fn start_reading_session_v2(
next_sequence: 1,
total_active_seconds: 0,
last_position: None,
+ last_checkpoint: None,
status: ReadingSessionStatus::Active,
};
@@ -168,13 +170,23 @@ pub fn get_active_session_v2(material_id: &str) -> Option {
}
}
-/// Record an event on this session: bumps sequence and updates last_event_at_ms.
-/// Returns the sequence number that should be assigned to the event.
pub fn record_session_event_v2(
session_id: &str,
timestamp_ms: i64,
active_seconds_delta: u32,
position: Option,
+) -> Result {
+ record_session_event_with_checkpoint_v2(session_id, timestamp_ms, active_seconds_delta, position, None)
+}
+
+/// Record an event on this session: bumps sequence and updates last_event_at_ms.
+/// Returns the sequence number that should be assigned to the event.
+pub fn record_session_event_with_checkpoint_v2(
+ session_id: &str,
+ timestamp_ms: i64,
+ active_seconds_delta: u32,
+ position: Option,
+ checkpoint: Option,
) -> Result {
let seq = with_session_mut(session_id, |s| {
if s.status == ReadingSessionStatus::Closed {
@@ -186,8 +198,12 @@ pub fn record_session_event_v2(
let seq = s.next_sequence;
s.next_sequence += 1;
s.last_event_at_ms = timestamp_ms;
- if position.is_some() {
- s.last_position = position;
+ if let Some(position) = position {
+ s.last_position = Some(position);
+ }
+ if let Some(checkpoint) = checkpoint {
+ s.last_position = Some(checkpoint.legacy_position());
+ s.last_checkpoint = Some(checkpoint);
}
Ok(seq)
})?;
@@ -318,11 +334,26 @@ impl std::fmt::Display for SessionError {
#[cfg(test)]
mod tests {
use super::*;
+ use std::sync::{Mutex, MutexGuard, OnceLock};
fn test_material() -> ReadingMaterialRefV2 {
ReadingMaterialRefV2::new("test_mat_001", "/tmp/test.md", "test.md", "markdown")
}
+ fn test_guard() -> MutexGuard<'static, ()> {
+ static GUARD: OnceLock> = OnceLock::new();
+ GUARD.get_or_init(|| Mutex::new(())).lock().unwrap()
+ }
+
+ fn reset_state() {
+ if let Ok(mut map) = sessions().lock() {
+ map.clear();
+ }
+ if let Ok(mut tmap) = trackers().lock() {
+ tmap.clear();
+ }
+ }
+
fn teardown(id: &str) {
let _ = close_reading_session_v2(id, 0);
let _ = remove_session_v2(id);
@@ -330,6 +361,8 @@ mod tests {
#[test]
fn test_start_session() {
+ let _guard = test_guard();
+ reset_state();
let id = start_reading_session_v2(test_material(), 1000).unwrap();
assert!(!id.is_empty());
let s = get_session_v2(&id).unwrap();
@@ -341,6 +374,8 @@ mod tests {
#[test]
fn test_pause_resume_close() {
+ let _guard = test_guard();
+ reset_state();
let id = start_reading_session_v2(test_material(), 1000).unwrap();
pause_reading_session_v2(&id).unwrap();
assert_eq!(get_session_v2(&id).unwrap().status, ReadingSessionStatus::Paused);
@@ -353,6 +388,8 @@ mod tests {
#[test]
fn test_remove_refuses_active() {
+ let _guard = test_guard();
+ reset_state();
let id = start_reading_session_v2(test_material(), 1000).unwrap();
assert!(remove_session_v2(&id).is_err()); // Not closed
teardown(&id);
@@ -360,6 +397,8 @@ mod tests {
#[test]
fn test_sequence_increments() {
+ let _guard = test_guard();
+ reset_state();
let id = start_reading_session_v2(test_material(), 1000).unwrap();
let s1 = record_session_event_v2(&id, 2000, 0, None).unwrap(); // tracker not used for 0 delta
let s2 = record_session_event_v2(&id, 3000, 0, None).unwrap();
@@ -370,6 +409,8 @@ mod tests {
#[test]
fn test_closed_rejects_events() {
+ let _guard = test_guard();
+ reset_state();
let id = start_reading_session_v2(test_material(), 1000).unwrap();
close_reading_session_v2(&id, 0).unwrap();
assert!(record_session_event_v2(&id, 2000, 10, None).is_err());
@@ -379,6 +420,8 @@ mod tests {
#[test]
fn test_paused_rejects_active_seconds() {
+ let _guard = test_guard();
+ reset_state();
let id = start_reading_session_v2(test_material(), 1000).unwrap();
pause_reading_session_v2(&id).unwrap();
// PositionChanged (delta=0) should be allowed when paused
@@ -391,12 +434,16 @@ mod tests {
#[test]
fn test_not_found() {
+ let _guard = test_guard();
+ reset_state();
assert!(get_session_v2("nonexistent").is_err());
assert!(close_reading_session_v2("nonexistent", 0).is_err());
}
#[test]
fn test_two_sessions_independent() {
+ let _guard = test_guard();
+ reset_state();
let a = start_reading_session_v2(test_material(), 1000).unwrap();
let b = start_reading_session_v2(ReadingMaterialRefV2::new("mat_b", "/tmp/b.md", "b.md", "markdown"), 2000).unwrap();
assert_ne!(a, b);
@@ -408,6 +455,8 @@ mod tests {
#[test]
fn test_total_active_seconds_accumulates() {
+ let _guard = test_guard();
+ reset_state();
let id = start_reading_session_v2(test_material(), 0).unwrap();
// Simulate iOS calling tick() and passing deltas to session events
record_session_event_v2(&id, 15_000, 15, None).unwrap();
@@ -420,6 +469,8 @@ mod tests {
#[test]
fn test_sequence_and_seconds_independent() {
+ let _guard = test_guard();
+ reset_state();
let a = start_reading_session_v2(test_material(), 0).unwrap();
let b = start_reading_session_v2(ReadingMaterialRefV2::new("mat_b", "/tmp/b.md", "b.md", "markdown"), 0).unwrap();
record_session_event_v2(&a, 10_000, 10, None).unwrap();
@@ -435,6 +486,8 @@ mod tests {
#[test]
fn test_last_position_updated() {
+ let _guard = test_guard();
+ reset_state();
let id = start_reading_session_v2(test_material(), 0).unwrap();
let pos1 = ReadingPosition::Markdown { block_id: "intro".into(), scroll_progress: 0.3 };
let pos2 = ReadingPosition::Markdown { block_id: "ch1".into(), scroll_progress: 0.8 };
@@ -446,6 +499,8 @@ mod tests {
#[test]
fn test_cleanup_stale_removes_old_sessions() {
+ let _guard = test_guard();
+ reset_state();
// Old session: last event at t=1000
let old = start_reading_session_v2(test_material(), 0).unwrap();
record_session_event_v2(&old, 1000, 0, None).unwrap();
@@ -468,6 +523,8 @@ mod tests {
#[test]
fn test_cleanup_stale_removes_orphaned_active_session() {
+ let _guard = test_guard();
+ reset_state();
// Simulate a crash: active session not closed, last event at t=5000
let orphaned = start_reading_session_v2(test_material(), 0).unwrap();
record_session_event_v2(&orphaned, 5000, 10, None).unwrap();
@@ -482,6 +539,8 @@ mod tests {
#[test]
fn test_cleanup_stale_zero_when_all_recent() {
+ let _guard = test_guard();
+ reset_state();
let active = start_reading_session_v2(test_material(), 0).unwrap();
// Last event at t=9500, now at t=10000, max_age=1000
// 10000-9500=500 < 1000 → NOT stale
@@ -498,6 +557,8 @@ mod tests {
#[test]
fn test_mark_interrupted() {
+ let _guard = test_guard();
+ reset_state();
let id = start_reading_session_v2(test_material(), 1000).unwrap();
mark_session_interrupted_v2(&id).unwrap();
assert_eq!(get_session_v2(&id).unwrap().status, ReadingSessionStatus::Interrupted);
@@ -508,6 +569,8 @@ mod tests {
#[test]
fn test_mark_failed() {
+ let _guard = test_guard();
+ reset_state();
let id = start_reading_session_v2(test_material(), 1000).unwrap();
mark_session_failed_v2(&id).unwrap();
assert_eq!(get_session_v2(&id).unwrap().status, ReadingSessionStatus::Failed);
@@ -518,6 +581,8 @@ mod tests {
#[test]
fn test_closed_rejects_interrupted() {
+ let _guard = test_guard();
+ reset_state();
let id = start_reading_session_v2(test_material(), 1000).unwrap();
close_reading_session_v2(&id, 0).unwrap();
assert!(mark_session_interrupted_v2(&id).is_err());
@@ -527,6 +592,8 @@ mod tests {
#[test]
fn test_close_sets_closed_at_ms() {
+ let _guard = test_guard();
+ reset_state();
let id = start_reading_session_v2(test_material(), 1000).unwrap();
assert!(get_session_v2(&id).unwrap().closed_at_ms.is_none());
close_reading_session_v2(&id, 5000).unwrap();
@@ -536,6 +603,8 @@ mod tests {
#[test]
fn test_set_closed_at_ms_override() {
+ let _guard = test_guard();
+ reset_state();
let id = start_reading_session_v2(test_material(), 1000).unwrap();
close_reading_session_v2(&id, 5000).unwrap();
// set_session_closed_at_ms can override if needed
@@ -546,6 +615,8 @@ mod tests {
#[test]
fn test_get_active_session_by_material() {
+ let _guard = test_guard();
+ reset_state();
let mat_a = ReadingMaterialRefV2::new("mat_a", "/tmp/a.md", "a.md", "markdown");
let mat_b = ReadingMaterialRefV2::new("mat_b", "/tmp/b.md", "b.md", "markdown");
@@ -565,6 +636,8 @@ mod tests {
#[test]
fn test_session_does_not_store_business_fields() {
+ let _guard = test_guard();
+ reset_state();
let id = start_reading_session_v2(test_material(), 1000).unwrap();
let s = get_session_v2(&id).unwrap();
// Verify the material ref has no business fields
@@ -576,6 +649,8 @@ mod tests {
#[test]
fn test_sequence_monotonic_in_session() {
+ let _guard = test_guard();
+ reset_state();
let id = start_reading_session_v2(test_material(), 1000).unwrap();
let s1 = record_session_event_v2(&id, 2000, 0, None).unwrap();
let s2 = record_session_event_v2(&id, 3000, 0, None).unwrap();
diff --git a/crates/zx_document_core/src/stable_id.rs b/crates/zx_document_core/src/stable_id.rs
new file mode 100644
index 0000000..72b8838
--- /dev/null
+++ b/crates/zx_document_core/src/stable_id.rs
@@ -0,0 +1,32 @@
+pub fn stable_content_id(namespace: &str, index: usize, content: &str) -> String {
+ let mut hash: u64 = 5381;
+
+ for byte in namespace
+ .bytes()
+ .chain(index.to_string().bytes())
+ .chain(content.bytes())
+ {
+ hash = ((hash << 5).wrapping_add(hash)).wrapping_add(byte as u64);
+ }
+
+ format!("{namespace}_{hash:016x}")
+}
+
+#[cfg(test)]
+mod tests {
+ use super::stable_content_id;
+
+ #[test]
+ fn stable_content_id_is_deterministic() {
+ let a = stable_content_id("text", 3, "hello");
+ let b = stable_content_id("text", 3, "hello");
+ assert_eq!(a, b);
+ }
+
+ #[test]
+ fn stable_content_id_changes_with_namespace_and_index() {
+ let base = stable_content_id("text", 3, "hello");
+ assert_ne!(base, stable_content_id("markdown", 3, "hello"));
+ assert_ne!(base, stable_content_id("text", 4, "hello"));
+ }
+}
diff --git a/crates/zx_document_core/src/text.rs b/crates/zx_document_core/src/text.rs
index d72892f..5f31b06 100644
--- a/crates/zx_document_core/src/text.rs
+++ b/crates/zx_document_core/src/text.rs
@@ -1,6 +1,6 @@
use crate::blocks::DocumentBlock;
use crate::error::DocumentError;
-use uuid::Uuid;
+use crate::stable_id::stable_content_id;
/// Read a TXT file and return its content as paragraph blocks.
pub fn read_text(file_path: &str) -> Result, DocumentError> {
@@ -18,11 +18,12 @@ pub fn parse_text_content(content: &str) -> Vec {
continue;
}
blocks.push(DocumentBlock::Paragraph {
- id: Uuid::new_v4().to_string(),
+ id: stable_content_id("text", blocks.len(), &text),
text,
});
}
+ eprintln!("[Rust] parse_text — len={} block_count={}", content.len(), blocks.len());
blocks
}
@@ -91,4 +92,11 @@ mod tests {
let result = read_text("/nonexistent/file.txt");
assert!(result.is_err());
}
+
+ #[test]
+ fn test_ids_are_stable() {
+ let first = parse_text_content("Hello\n\nWorld");
+ let second = parse_text_content("Hello\n\nWorld");
+ assert_eq!(first, second);
+ }
}
diff --git a/crates/zx_document_ffi/src/lib.rs b/crates/zx_document_ffi/src/lib.rs
index 5e4c9e4..5e4018e 100644
--- a/crates/zx_document_ffi/src/lib.rs
+++ b/crates/zx_document_ffi/src/lib.rs
@@ -10,7 +10,7 @@ pub use zx_document_core::image_meta::ImageMeta;
pub use zx_document_core::text::TextStats;
pub use zx_document_core::search::SearchResult;
pub use zx_document_core::anchors::NoteAnchor;
-pub use zx_document_core::progress::ReadingPosition;
+pub use zx_document_core::progress::{ReadingCheckpoint, ReadingPosition, RendererKind};
pub use zx_document_core::events::ReadingEvent;
#[allow(deprecated)]
pub use zx_document_core::reading_material::ReadingMaterialRef;
@@ -26,39 +26,42 @@ use zx_document_core::blocks as core_blocks;
// ── V2 Reading Session FFI ──
#[uniffi::export]
-fn start_reading_session_v2(material: ReadingMaterialRefV2, timestamp_ms: i64) -> Result {
- zx_document_core::session_v2::start_reading_session_v2(material, timestamp_ms)
- .map_err(|e| e.to_string())
+fn start_reading_session_v2(material: ReadingMaterialRefV2, timestamp_ms: i64) -> Result {
+ let r = zx_document_core::session_v2::start_reading_session_v2(material, timestamp_ms)
+ .map_err(Into::into);
+ match &r { Ok(sid) => eprintln!("[Rust] start_session OK — sid={}", sid), Err(e) => eprintln!("[Rust] start_session FAILED — {:?}", e) }
+ r
}
#[uniffi::export]
-fn pause_reading_session_v2(session_id: String) -> Result<(), String> {
- zx_document_core::session_v2::pause_reading_session_v2(&session_id).map_err(|e| e.to_string())
+fn pause_reading_session_v2(session_id: String) -> Result<(), DocumentError> {
+ zx_document_core::session_v2::pause_reading_session_v2(&session_id).map_err(Into::into)
}
#[uniffi::export]
-fn resume_reading_session_v2(session_id: String) -> Result<(), String> {
- zx_document_core::session_v2::resume_reading_session_v2(&session_id).map_err(|e| e.to_string())
+fn resume_reading_session_v2(session_id: String) -> Result<(), DocumentError> {
+ zx_document_core::session_v2::resume_reading_session_v2(&session_id).map_err(Into::into)
}
#[uniffi::export]
-fn close_reading_session_v2(session_id: String, timestamp_ms: i64) -> Result<(), String> {
- zx_document_core::session_v2::close_reading_session_v2(&session_id, timestamp_ms).map_err(|e| e.to_string())
+fn close_reading_session_v2(session_id: String, timestamp_ms: i64) -> Result<(), DocumentError> {
+ eprintln!("[Rust] close_session — sid={}", session_id);
+ zx_document_core::session_v2::close_reading_session_v2(&session_id, timestamp_ms).map_err(Into::into)
}
#[uniffi::export]
-fn set_session_closed_at_ms_v2(session_id: String, timestamp_ms: i64) -> Result<(), String> {
- zx_document_core::session_v2::set_session_closed_at_ms(&session_id, timestamp_ms).map_err(|e| e.to_string())
+fn set_session_closed_at_ms_v2(session_id: String, timestamp_ms: i64) -> Result<(), DocumentError> {
+ zx_document_core::session_v2::set_session_closed_at_ms(&session_id, timestamp_ms).map_err(Into::into)
}
#[uniffi::export]
-fn mark_session_interrupted_v2(session_id: String) -> Result<(), String> {
- zx_document_core::session_v2::mark_session_interrupted_v2(&session_id).map_err(|e| e.to_string())
+fn mark_session_interrupted_v2(session_id: String) -> Result<(), DocumentError> {
+ zx_document_core::session_v2::mark_session_interrupted_v2(&session_id).map_err(Into::into)
}
#[uniffi::export]
-fn mark_session_failed_v2(session_id: String) -> Result<(), String> {
- zx_document_core::session_v2::mark_session_failed_v2(&session_id).map_err(|e| e.to_string())
+fn mark_session_failed_v2(session_id: String) -> Result<(), DocumentError> {
+ zx_document_core::session_v2::mark_session_failed_v2(&session_id).map_err(Into::into)
}
#[uniffi::export]
@@ -74,45 +77,59 @@ fn get_session_v2_ffi(session_id: String) -> Option {
// ── V2 Reading Event FFI ──
#[uniffi::export]
-fn push_material_opened_v2(session_id: String, material_id: String, timestamp_ms: i64) -> Result {
- zx_document_core::events_v2::push_material_opened_v2(&session_id, &material_id, timestamp_ms)
- .map_err(|e| e.to_string())
+fn push_material_opened_v2(session_id: String, material_id: String, timestamp_ms: i64) -> Result {
+ eprintln!("[Rust] push_material_opened — mid={}", material_id);
+ zx_document_core::events_v2::push_material_opened_v2(&session_id, &material_id, timestamp_ms).map_err(Into::into)
}
#[uniffi::export]
-fn push_material_closed_v2(session_id: String, material_id: String, active_seconds_delta: u32, timestamp_ms: i64) -> Result {
- zx_document_core::events_v2::push_material_closed_v2(&session_id, &material_id, active_seconds_delta, timestamp_ms)
- .map_err(|e| e.to_string())
+fn push_material_closed_v2(session_id: String, material_id: String, active_seconds_delta: u32, timestamp_ms: i64) -> Result {
+ eprintln!("[Rust] push_material_closed — mid={} active_s={}", material_id, active_seconds_delta);
+ zx_document_core::events_v2::push_material_closed_v2(&session_id, &material_id, active_seconds_delta, timestamp_ms).map_err(Into::into)
}
#[uniffi::export]
-fn push_position_changed_v2(session_id: String, material_id: String, position: ReadingPosition, timestamp_ms: i64) -> Result {
- zx_document_core::events_v2::push_position_changed_v2(&session_id, &material_id, position, timestamp_ms)
- .map_err(|e| e.to_string())
+fn push_position_changed_v2(session_id: String, material_id: String, position: ReadingPosition, timestamp_ms: i64) -> Result {
+ eprintln!("[Rust] push_position_changed — mid={}", material_id);
+ zx_document_core::events_v2::push_position_changed_v2(&session_id, &material_id, position, timestamp_ms).map_err(Into::into)
}
#[uniffi::export]
-fn push_heartbeat_v2(session_id: String, material_id: String, active_seconds_delta: u32, position: Option, timestamp_ms: i64) -> Result {
- zx_document_core::events_v2::push_heartbeat_v2(&session_id, &material_id, active_seconds_delta, position, timestamp_ms)
- .map_err(|e| e.to_string())
+fn push_heartbeat_v2(session_id: String, material_id: String, active_seconds_delta: u32, position: Option, timestamp_ms: i64) -> Result {
+ zx_document_core::events_v2::push_heartbeat_v2(&session_id, &material_id, active_seconds_delta, position, timestamp_ms).map_err(Into::into)
}
#[uniffi::export]
-fn push_marked_as_read_v2(session_id: String, material_id: String, timestamp_ms: i64) -> Result {
- zx_document_core::events_v2::push_marked_as_read_v2(&session_id, &material_id, timestamp_ms)
- .map_err(|e| e.to_string())
+fn push_checkpoint_changed_v2(session_id: String, material_id: String, checkpoint: ReadingCheckpoint, timestamp_ms: i64) -> Result {
+ eprintln!("[Rust] push_checkpoint_changed — mid={}", material_id);
+ zx_document_core::events_v2::push_checkpoint_changed_v2(&session_id, &material_id, checkpoint, timestamp_ms).map_err(Into::into)
+}
+
+#[uniffi::export]
+fn push_heartbeat_checkpoint_v2(session_id: String, material_id: String, active_seconds_delta: u32, checkpoint: Option, timestamp_ms: i64) -> Result {
+ zx_document_core::events_v2::push_heartbeat_checkpoint_v2(&session_id, &material_id, active_seconds_delta, checkpoint, timestamp_ms).map_err(Into::into)
+}
+
+#[uniffi::export]
+fn push_marked_as_read_v2(session_id: String, material_id: String, timestamp_ms: i64) -> Result {
+ eprintln!("[Rust] push_marked_as_read — mid={}", material_id);
+ zx_document_core::events_v2::push_marked_as_read_v2(&session_id, &material_id, timestamp_ms).map_err(Into::into)
}
// ── V2 Buffer Management FFI ──
#[uniffi::export]
fn export_pending_events_v2(limit: u32, timestamp_ms: i64) -> Vec {
- zx_document_core::events_v2::export_pending_events_v2(limit, timestamp_ms)
+ let events = zx_document_core::events_v2::export_pending_events_v2(limit, timestamp_ms);
+ if !events.is_empty() { eprintln!("[Rust] export_events — count={}", events.len()); }
+ events
}
#[uniffi::export]
fn reload_stale_events_v2() -> u32 {
- zx_document_core::events_v2::reload_stale_events_v2()
+ let n = zx_document_core::events_v2::reload_stale_events_v2();
+ if n > 0 { eprintln!("[Rust] reload_stale — count={}", n); }
+ n
}
#[uniffi::export]
@@ -122,14 +139,26 @@ fn get_event_buffer_state_v2() -> EventBufferStateV2 {
#[uniffi::export]
fn ack_events_v2(event_ids: Vec) -> u32 {
+ eprintln!("[Rust] ack_events — count={}", event_ids.len());
zx_document_core::events_v2::ack_events_v2(&event_ids)
}
#[uniffi::export]
fn mark_events_failed_v2(event_ids: Vec) -> u32 {
+ eprintln!("[Rust] mark_failed — count={}", event_ids.len());
zx_document_core::events_v2::mark_events_failed_v2(&event_ids)
}
+#[uniffi::export]
+fn compute_file_fingerprint(file_path: String) -> Result {
+ zx_document_core::fingerprint::compute_file_fingerprint(&file_path).map_err(Into::into)
+}
+
+#[uniffi::export]
+fn build_representation_fingerprint(source_fingerprint: String, renderer_kind: RendererKind, parser_version: String, schema_version: u32) -> String {
+ zx_document_core::fingerprint::build_representation_fingerprint(&source_fingerprint, &renderer_kind, &parser_version, schema_version)
+}
+
// FFI-compatible DocumentBlock (tuple variants, UniFFI proc-macro)
#[derive(Debug, uniffi::Enum)]
pub enum DocumentBlock {
@@ -181,6 +210,11 @@ pub enum DocumentError {
ParseError,
InvalidEncoding,
IoError,
+ SessionNotFound,
+ SessionAlreadyActive,
+ SessionNotActive,
+ BufferOverflow,
+ Unsupported,
}
impl std::fmt::Display for DocumentError {
@@ -191,6 +225,11 @@ impl std::fmt::Display for DocumentError {
Self::ParseError => write!(f, "Parse error"),
Self::InvalidEncoding => write!(f, "Invalid encoding"),
Self::IoError => write!(f, "IO error"),
+ Self::SessionNotFound => write!(f, "Session not found"),
+ Self::SessionAlreadyActive => write!(f, "Session already active"),
+ Self::SessionNotActive => write!(f, "Session not active"),
+ Self::BufferOverflow => write!(f, "Buffer overflow"),
+ Self::Unsupported => write!(f, "Unsupported"),
}
}
}
@@ -209,6 +248,20 @@ impl From for DocumentError {
}
}
+impl From for DocumentError {
+ fn from(e: zx_document_core::session_v2::SessionError) -> Self {
+ match e {
+ zx_document_core::session_v2::SessionError::NotFound => Self::SessionNotFound,
+ zx_document_core::session_v2::SessionError::AlreadyClosed
+ | zx_document_core::session_v2::SessionError::AlreadyInterrupted
+ | zx_document_core::session_v2::SessionError::AlreadyFailed => Self::SessionAlreadyActive,
+ zx_document_core::session_v2::SessionError::NotActive
+ | zx_document_core::session_v2::SessionError::NotClosed => Self::SessionNotActive,
+ zx_document_core::session_v2::SessionError::LockPoisoned => Self::Unsupported,
+ }
+ }
+}
+
#[uniffi::export]
fn detect_material_type(file_path: String) -> Result {
zx_document_core::material_type::detect_material_type(&file_path).map_err(Into::into)
diff --git a/crates/zx_document_ffi/src/zx_document.udl b/crates/zx_document_ffi/src/zx_document.udl
index a979911..2e3408b 100644
--- a/crates/zx_document_ffi/src/zx_document.udl
+++ b/crates/zx_document_ffi/src/zx_document.udl
@@ -49,7 +49,7 @@ namespace zx_document {
// ── V2 Reading Session ──
[Throws=DocumentError]
- string start_reading_session_v2(ReadingMaterialRef material, i64 timestamp_ms);
+ string start_reading_session_v2(ReadingMaterialRefV2 material, i64 timestamp_ms);
[Throws=DocumentError]
void pause_reading_session_v2([ByRef] string session_id);
@@ -58,7 +58,20 @@ namespace zx_document {
void resume_reading_session_v2([ByRef] string session_id);
[Throws=DocumentError]
- void close_reading_session_v2([ByRef] string session_id);
+ void close_reading_session_v2([ByRef] string session_id, i64 timestamp_ms);
+
+ ReadingSessionV2? get_active_session_v2([ByRef] string material_id);
+
+ ReadingSessionV2? get_session_v2_ffi([ByRef] string session_id);
+
+ [Throws=DocumentError]
+ void mark_session_interrupted_v2([ByRef] string session_id);
+
+ [Throws=DocumentError]
+ void mark_session_failed_v2([ByRef] string session_id);
+
+ [Throws=DocumentError]
+ void set_session_closed_at_ms_v2([ByRef] string session_id, i64 timestamp_ms);
// ── V2 Reading Events ──
[Throws=DocumentError]
@@ -73,6 +86,12 @@ namespace zx_document {
[Throws=DocumentError]
ReadingEventV2 push_heartbeat_v2([ByRef] string session_id, [ByRef] string material_id, u32 active_seconds_delta, ReadingPosition? position, i64 timestamp_ms);
+ [Throws=DocumentError]
+ ReadingEventV2 push_checkpoint_changed_v2([ByRef] string session_id, [ByRef] string material_id, ReadingCheckpoint checkpoint, i64 timestamp_ms);
+
+ [Throws=DocumentError]
+ ReadingEventV2 push_heartbeat_checkpoint_v2([ByRef] string session_id, [ByRef] string material_id, u32 active_seconds_delta, ReadingCheckpoint? checkpoint, i64 timestamp_ms);
+
[Throws=DocumentError]
ReadingEventV2 push_marked_as_read_v2([ByRef] string session_id, [ByRef] string material_id, i64 timestamp_ms);
@@ -83,8 +102,15 @@ namespace zx_document {
u32 mark_events_failed_v2(sequence event_ids);
+ [Throws=DocumentError]
+ string compute_file_fingerprint([ByRef] string file_path);
+
+ string build_representation_fingerprint([ByRef] string source_fingerprint, RendererKind renderer_kind, [ByRef] string parser_version, u32 schema_version);
+
u32 reload_stale_events_v2();
+ EventBufferStateV2 get_event_buffer_state_v2();
+
// ── V1 (deprecated) ──
void push_reading_event(ReadingEvent event);
void update_reading_position([ByRef] string material_id, ReadingPosition position);
@@ -99,6 +125,11 @@ enum DocumentError {
"ParseError",
"InvalidEncoding",
"IoError",
+ "SessionNotFound",
+ "SessionAlreadyActive",
+ "SessionNotActive",
+ "BufferOverflow",
+ "Unsupported",
};
// ── V2 Types ──
@@ -107,24 +138,46 @@ dictionary ReadingMaterialRef {
string material_id;
};
+dictionary ReadingMaterialRefV2 {
+ string material_id;
+ string file_path;
+ string file_name;
+ string file_type;
+ string? mime_type;
+ u64? file_size;
+ string? content_hash;
+ i64? created_at_ms;
+};
+
[Enum]
interface ReadingSessionStatus {
Active();
Paused();
Closed();
+ Interrupted();
+ Failed();
};
dictionary ReadingSessionV2 {
string client_session_id;
- ReadingMaterialRef material;
+ ReadingMaterialRefV2 material;
i64 started_at_ms;
+ i64? closed_at_ms;
i64 last_event_at_ms;
u64 next_sequence;
u32 total_active_seconds;
ReadingPosition? last_position;
+ ReadingCheckpoint? last_checkpoint;
ReadingSessionStatus status;
};
+dictionary EventBufferStateV2 {
+ u32 total;
+ u32 pending;
+ u32 exported;
+ u32 failed;
+};
+
[Enum]
interface ReadingEventTypeV2 {
MaterialOpened();
@@ -140,11 +193,34 @@ dictionary ReadingEventV2 {
string material_id;
ReadingEventTypeV2 event_type;
ReadingPosition? position;
+ ReadingCheckpoint? checkpoint;
u32 active_seconds_delta;
i64 timestamp_ms;
u64 sequence;
};
+[Enum]
+interface RendererKind {
+ Block();
+ Pdf();
+ Image();
+ Workbook();
+ Slide();
+ Epub();
+ Unknown();
+};
+
+dictionary ReadingCheckpoint {
+ u32 schema_version;
+ string material_id;
+ string representation_fingerprint;
+ RendererKind renderer_kind;
+ u64 captured_at_ms;
+ string device_id;
+ u64 device_sequence;
+ ReadingPosition position;
+};
+
[Enum]
interface MaterialType {
Markdown();
@@ -188,10 +264,15 @@ dictionary DocumentInfo {
[Enum]
interface ReadingPosition {
+ Block(string block_id, f32 block_progress, f32 overall_progress);
Markdown(string block_id, f32 scroll_progress);
Text(u32 line_number, f32 scroll_progress);
Pdf(u32 page_number, f32 page_progress, f32 overall_progress);
+ PdfViewport(u32 page_number, f32 anchor_x, f32 anchor_y, f32 zoom_scale, f32 overall_progress);
Image(f32 zoom_scale, f32 offset_x, f32 offset_y);
+ ImageViewport(f32 zoom_scale, f32 anchor_x, f32 anchor_y);
+ Workbook(string sheet_id, u32 top_row, u32 left_column, string? selected_cell, f32 zoom_scale, f32? overall_progress);
+ Slide(u32 slide_index, f32 zoom_scale, f32 anchor_x, f32 anchor_y, f32 overall_progress);
Epub(string chapter_id, f32 chapter_progress, f32 overall_progress);
Unknown();
};
diff --git a/scripts/build-ios.sh b/scripts/build-ios.sh
index 49c127a..104ff70 100755
--- a/scripts/build-ios.sh
+++ b/scripts/build-ios.sh
@@ -7,7 +7,7 @@ RUST_DIR="$(cd "$(dirname "$0")/.." && pwd)"
OUT_DIR="$RUST_DIR/bindings/ios"
LIB_NAME="libzx_document_ffi.a"
-rustup target add aarch64-apple-ios aarch64-apple-ios-sim
+rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios
echo "==> Building for iOS device (arm64)..."
cargo build --release --target aarch64-apple-ios -p zx_document_ffi
@@ -15,11 +15,21 @@ cargo build --release --target aarch64-apple-ios -p zx_document_ffi
echo "==> Building for iOS simulator (arm64)..."
cargo build --release --target aarch64-apple-ios-sim -p zx_document_ffi
+echo "==> Building for iOS simulator (x86_64)..."
+cargo build --release --target x86_64-apple-ios -p zx_document_ffi
+
rm -rf "$OUT_DIR/ZxDocumentRuntime.xcframework"
mkdir -p "$OUT_DIR/device" "$OUT_DIR/simulator" "$OUT_DIR/generated"
cp "target/aarch64-apple-ios/release/$LIB_NAME" "$OUT_DIR/device/"
-cp "target/aarch64-apple-ios-sim/release/$LIB_NAME" "$OUT_DIR/simulator/"
+cp "target/aarch64-apple-ios-sim/release/$LIB_NAME" "$OUT_DIR/simulator/libzx_document_ffi_arm64.a"
+cp "target/x86_64-apple-ios/release/$LIB_NAME" "$OUT_DIR/simulator/libzx_document_ffi_x86_64.a"
+
+echo "==> Creating universal iOS simulator static library..."
+lipo -create \
+ "$OUT_DIR/simulator/libzx_document_ffi_arm64.a" \
+ "$OUT_DIR/simulator/libzx_document_ffi_x86_64.a" \
+ -output "$OUT_DIR/simulator/$LIB_NAME"
echo "==> Generating Swift bindings (UDL bindgen)..."
rm -f "$OUT_DIR/generated/zx_document.swift"
diff --git a/scripts/check-symbols.sh b/scripts/check-symbols.sh
index 4dfc7c7..7ccdc81 100755
--- a/scripts/check-symbols.sh
+++ b/scripts/check-symbols.sh
@@ -38,6 +38,27 @@ check uniffi_zx_document_ffi_fn_func_update_reading_position
check uniffi_zx_document_ffi_fn_func_export_pending_events
check uniffi_zx_document_ffi_fn_func_clear_exported_events
+# V2 session lifecycle
+check uniffi_zx_document_ffi_fn_func_start_reading_session_v2
+check uniffi_zx_document_ffi_fn_func_pause_reading_session_v2
+check uniffi_zx_document_ffi_fn_func_resume_reading_session_v2
+check uniffi_zx_document_ffi_fn_func_close_reading_session_v2
+
+# V2 event push
+check uniffi_zx_document_ffi_fn_func_push_material_opened_v2
+check uniffi_zx_document_ffi_fn_func_push_material_closed_v2
+check uniffi_zx_document_ffi_fn_func_push_position_changed_v2
+check uniffi_zx_document_ffi_fn_func_push_heartbeat_v2
+check uniffi_zx_document_ffi_fn_func_push_marked_as_read_v2
+
+# V2 buffer management
+check uniffi_zx_document_ffi_fn_func_export_pending_events_v2
+check uniffi_zx_document_ffi_fn_func_ack_events_v2
+check uniffi_zx_document_ffi_fn_func_mark_events_failed_v2
+check uniffi_zx_document_ffi_fn_func_reload_stale_events_v2
+check uniffi_zx_document_ffi_fn_func_get_event_buffer_state_v2
+check uniffi_zx_document_ffi_fn_func_cleanup_stale_sessions_ffi
+
# Buffer management
check ffi_zx_document_ffi_rustbuffer_alloc
check ffi_zx_document_ffi_rustbuffer_from_bytes
diff --git a/scripts/patch-swift.py b/scripts/patch-swift.py
index 990aa82..b956915 100755
--- a/scripts/patch-swift.py
+++ b/scripts/patch-swift.py
@@ -12,11 +12,13 @@ c = c.replace(
"fileprivate extension Data {\n init(rustBuffer: RustBuffer) {\n let buf = UnsafeRawBufferPointer(start: rustBuffer.data, count: Int(rustBuffer.len))\n self.init(buf)\n }\n}"
)
-# Patch 2: Fix UnsafeMutableRawPointer cast for iOS 26 SDK
-c = c.replace(
- "let bytes = UnsafeBufferPointer(start: value.data!, count: Int(value.len))",
- "let bytes = UnsafeBufferPointer(start: value.data!.assumingMemoryBound(to: UInt8.self), count: Int(value.len))"
-)
+# Patch 2: removed — `RustBuffer.data` is now `UnsafeMutablePointer?` on current SDKs,
+# so `.assumingMemoryBound(to:)` is not needed (and would cause a compile error).
+# Kept as comment in case an older SDK needs the raw-pointer cast.
+# c = c.replace(
+# "let bytes = UnsafeBufferPointer(start: value.data!, count: Int(value.len))",
+# "let bytes = UnsafeBufferPointer(start: value.data!.assumingMemoryBound(to: UInt8.self), count: Int(value.len))"
+# )
# Patch 3: Remove checksum initialization
# UniFFI 0.28 UDL scaffolding does not export checksum functions as #[no_mangle],