@dittolive/ditto 4.5.4 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +2 -2
  2. package/node/ditto.cjs.js +1111 -563
  3. package/node/ditto.darwin-arm64.node +0 -0
  4. package/node/ditto.darwin-x64.node +0 -0
  5. package/node/ditto.linux-arm.node +0 -0
  6. package/node/ditto.linux-arm64.node +0 -0
  7. package/node/ditto.linux-x64.node +0 -0
  8. package/node/ditto.win32-x64.node +0 -0
  9. package/node/transports.darwin-arm64.node +0 -0
  10. package/node/transports.darwin-x64.node +0 -0
  11. package/package.json +45 -1
  12. package/react-native/android/build.gradle +1 -1
  13. package/react-native/android/cpp-adapter.cpp +68 -59
  14. package/react-native/android/src/main/java/com/dittolive/rnsdk/DittoRNSDKModule.java +35 -0
  15. package/react-native/cpp/include/Arc.hpp +1 -1
  16. package/react-native/cpp/include/Attachment.h +5 -1
  17. package/react-native/cpp/include/DQL.h +9 -9
  18. package/react-native/cpp/include/FFIUtils.h +14 -0
  19. package/react-native/cpp/include/IO.h +13 -0
  20. package/react-native/cpp/include/Lifecycle.h +0 -1
  21. package/react-native/cpp/include/Misc.h +3 -0
  22. package/react-native/cpp/include/Utils.h +0 -2
  23. package/react-native/cpp/src/Attachment.cpp +200 -13
  24. package/react-native/cpp/src/Authentication.cpp +3 -3
  25. package/react-native/cpp/src/DQL.cpp +23 -23
  26. package/react-native/cpp/src/Document.cpp +10 -10
  27. package/react-native/cpp/src/FFIUtils.cpp +64 -0
  28. package/react-native/cpp/src/IO.cpp +35 -0
  29. package/react-native/cpp/src/Identity.cpp +3 -3
  30. package/react-native/cpp/src/Lifecycle.cpp +2 -19
  31. package/react-native/cpp/src/LiveQuery.cpp +3 -5
  32. package/react-native/cpp/src/Logger.cpp +171 -172
  33. package/react-native/cpp/src/Misc.cpp +52 -4
  34. package/react-native/cpp/src/Presence.cpp +1 -1
  35. package/react-native/cpp/src/SmallPeerInfo.cpp +1 -1
  36. package/react-native/cpp/src/Transports.cpp +10 -5
  37. package/react-native/cpp/src/Utils.cpp +110 -114
  38. package/react-native/cpp/src/main.cpp +28 -15
  39. package/react-native/dittoffi/dittoffi.h +328 -280
  40. package/react-native/ios/DittoRNSDK.mm +123 -71
  41. package/react-native/src/ditto.rn.ts +30 -6
  42. package/react-native/src/index.ts +7 -4
  43. package/react-native/src/sources/@cbor-redux.ts +1 -1
  44. package/react-native/src/sources/attachment-fetch-event.ts +2 -2
  45. package/react-native/src/sources/attachment-fetcher-manager.ts +5 -4
  46. package/react-native/src/sources/attachment-fetcher.ts +152 -21
  47. package/react-native/src/sources/attachment-token.ts +94 -13
  48. package/react-native/src/sources/attachment.ts +66 -19
  49. package/react-native/src/sources/augment.ts +13 -6
  50. package/react-native/src/sources/base-pending-cursor-operation.ts +22 -6
  51. package/react-native/src/sources/base-pending-id-specific-operation.ts +3 -0
  52. package/react-native/src/sources/bridge.ts +2 -2
  53. package/react-native/src/sources/cbor.ts +0 -15
  54. package/react-native/src/sources/collection-interface.ts +12 -6
  55. package/react-native/src/sources/collection.ts +9 -2
  56. package/react-native/src/sources/ditto.ts +26 -18
  57. package/react-native/src/sources/document-id.ts +11 -7
  58. package/react-native/src/sources/document-path.ts +4 -2
  59. package/react-native/src/sources/document.ts +49 -5
  60. package/react-native/src/sources/error-codes.ts +28 -0
  61. package/react-native/src/sources/error.ts +20 -1
  62. package/react-native/src/sources/essentials.ts +25 -3
  63. package/react-native/src/sources/ffi-error.ts +2 -1
  64. package/react-native/src/sources/ffi.ts +180 -102
  65. package/react-native/src/sources/internal.ts +37 -3
  66. package/react-native/src/sources/live-query-manager.ts +10 -1
  67. package/react-native/src/sources/live-query.ts +1 -1
  68. package/react-native/src/sources/observer-manager.ts +7 -0
  69. package/react-native/src/sources/pending-id-specific-operation.ts +2 -2
  70. package/react-native/src/sources/presence-manager.ts +12 -2
  71. package/react-native/src/sources/presence.ts +5 -0
  72. package/react-native/src/sources/query-result-item.ts +15 -0
  73. package/react-native/src/sources/small-peer-info.ts +2 -2
  74. package/react-native/src/sources/static-tcp-client.ts +2 -0
  75. package/react-native/src/sources/store-observer.ts +4 -2
  76. package/react-native/src/sources/store.ts +253 -3
  77. package/react-native/src/sources/sync.ts +6 -3
  78. package/react-native/src/sources/transport-config.ts +2 -2
  79. package/react-native/src/sources/update-results-map.ts +8 -0
  80. package/react-native/src/sources/write-transaction-collection.ts +1 -1
  81. package/react-native/src/sources/write-transaction.ts +1 -1
  82. package/types/ditto.d.ts +2866 -2568
  83. package/web/ditto.es6.js +1 -1
  84. package/web/ditto.umd.js +1 -1
  85. package/web/ditto.wasm +0 -0
  86. package/react-native/.yarn/install-state.gz +0 -0
  87. package/react-native/.yarnrc.yml +0 -1
@@ -3,24 +3,68 @@
3
3
  //
4
4
 
5
5
  import * as FFI from './ffi'
6
+ import { mapFFIErrors } from './error'
7
+
8
+ import type { AttachmentMetadata } from './attachment'
9
+ import type { Store } from './store'
10
+
11
+ /** @internal */
12
+ export type UntypedAttachmentToken = { id: string; len: number | BigInt; metadata: AttachmentMetadata }
13
+
14
+ /** @internal */
15
+ export type TypedAttachmentToken = { [FFI.DittoCRDTTypeKey]?: FFI.DittoCRDTType.attachment; _id: Uint8Array; _len: number | BigInt; _meta: AttachmentMetadata }
6
16
 
7
17
  /**
8
18
  * Serves as a token for a specific attachment that you can pass to a call to
9
- * {@link Collection.fetchAttachment | fetchAttachment()} on a
10
- * {@link Collection}.
19
+ * {@link Store.fetchAttachment | ditto.store.fetchAttachment()}.
11
20
  */
12
21
  export class AttachmentToken {
13
- /** @internal */
14
- readonly id: Uint8Array
22
+ /** The attachment's ID. */
23
+ // This ID is a _non-padded_ base64-encoded version of `idBytes`.
24
+ readonly id: string
15
25
 
16
- /** @internal */
17
- readonly len: number
26
+ /** The attachment's size given as number of bytes. */
27
+ readonly len: number | BigInt
28
+
29
+ /** The attachment's metadata. */
30
+ readonly metadata: AttachmentMetadata
31
+
32
+ // -------------------------------------------------------------------------
18
33
 
19
34
  /** @internal */
20
- readonly metadata: { [key: string]: string }
35
+ constructor(jsObj: UntypedAttachmentToken | TypedAttachmentToken) {
36
+ // There are two representations of attachment tokens:
37
+ // 1. The legacy typed representation is an internal format that is used by
38
+ // the query builder API. It can be identified by the presence of the
39
+ // [FFI.DittoCRDTTypeKey] field.
40
+ // 2. The untyped representation is used in our public API and was first
41
+ // introduced in the HTTP API. It is now used by the DQL API as well. It
42
+ // uses a non-padded base64-encoded ID.
43
+ let id: Uint8Array, len: number | BigInt, meta: AttachmentMetadata
44
+ if (jsObj[FFI.DittoCRDTTypeKey] != null) {
45
+ ;({ id, len, meta } = AttachmentToken.validateTypedInput(jsObj as TypedAttachmentToken))
46
+ } else {
47
+ ;({ id, len, meta } = AttachmentToken.validateUntypedInput(jsObj as UntypedAttachmentToken))
48
+ }
49
+
50
+ // base64-encode string and remove padding
51
+ this.id = mapFFIErrors(() => FFI.base64encode(id, 'Unpadded'))
52
+ this.idBytes = id
53
+ this.len = len
54
+ this.metadata = meta
55
+ }
21
56
 
22
57
  /** @internal */
23
- constructor(jsObj: object) {
58
+ readonly idBytes: Uint8Array
59
+
60
+ /**
61
+ * Validate an input value that has a field `[FFI.DittoCRDTTypeKey]` and
62
+ * return its contents.
63
+ *
64
+ * @throws {Error} If the input is invalid.
65
+ * @returns {object} binary id, len and metadata of the attachment token
66
+ */
67
+ private static validateTypedInput(jsObj: TypedAttachmentToken): { id: Uint8Array; len: number | BigInt; meta: AttachmentMetadata } {
24
68
  const type = jsObj[FFI.DittoCRDTTypeKey]
25
69
  if (type !== FFI.DittoCRDTType.attachment) {
26
70
  throw new Error('Invalid attachment token')
@@ -32,8 +76,8 @@ export class AttachmentToken {
32
76
  }
33
77
 
34
78
  const len = jsObj['_len']
35
- if (typeof len !== 'number' || len < 0) {
36
- throw new Error('Invalid attachment token length')
79
+ if ((typeof len !== 'number' && typeof len !== 'bigint') || len < 0) {
80
+ throw new Error('Invalid attachment token length, must be a non-negative number or bigint')
37
81
  }
38
82
 
39
83
  const meta = jsObj['_meta']
@@ -41,8 +85,45 @@ export class AttachmentToken {
41
85
  throw new Error('Invalid attachment token meta')
42
86
  }
43
87
 
44
- this.id = id
45
- this.len = len
46
- this.metadata = meta
88
+ return { id, len, meta }
89
+ }
90
+
91
+ /**
92
+ * Validate an untyped input value and return its contents.
93
+ *
94
+ * Converts _unpadded_ base64-encoded ID in input to _padded_ base64-encoded
95
+ * ID before returning it as `Uint8Array`.
96
+ *
97
+ * @throws {@link DittoError} `store/attachment-token-invalid` If the input id
98
+ * is not a valid base64 string.
99
+ * @returns {object} binary id, len and metadata of the attachment token
100
+ */
101
+ private static validateUntypedInput(jsObj: UntypedAttachmentToken): { id: Uint8Array; len: number | BigInt; meta: AttachmentMetadata } {
102
+ const idBase64 = jsObj['id']
103
+ if (typeof idBase64 !== 'string') {
104
+ throw new Error('Invalid attachment token id')
105
+ }
106
+
107
+ const id = mapFFIErrors(
108
+ () => FFI.tryBase64Decode(idBase64, 'Unpadded'),
109
+ {
110
+ Base64Invalid: ['store/attachment-token-invalid', 'Failed to decode attachment token id from base64 input'],
111
+ },
112
+ {
113
+ attachmentTokenID: idBase64,
114
+ },
115
+ )
116
+
117
+ const len = jsObj['len']
118
+ if ((typeof len !== 'number' && typeof len !== 'bigint') || len < 0) {
119
+ throw new Error('Invalid attachment token length, must be a non-negative number or bigint')
120
+ }
121
+
122
+ const meta = jsObj['metadata']
123
+ if (typeof meta !== 'object') {
124
+ throw new Error('Invalid attachment token meta')
125
+ }
126
+
127
+ return { id, len, meta }
47
128
  }
48
129
  }
@@ -3,17 +3,23 @@
3
3
  //
4
4
 
5
5
  import * as FFI from './ffi'
6
- import { Bridge } from './bridge'
7
6
  import * as Environment from './@environment'
7
+ import { Bridge } from './bridge'
8
+ import { DittoError } from './error'
8
9
 
9
10
  import type { Ditto } from './ditto'
10
11
  import type { AttachmentToken } from './attachment-token'
11
12
 
13
+ /**
14
+ * A key-value map of user-defined metadata for an attachment.
15
+ */
16
+ export type AttachmentMetadata = { [key: string]: string }
17
+
12
18
  /**
13
19
  * Represents an attachment and can be used to insert the associated attachment
14
20
  * into a document at a specific key-path. You can't instantiate an attachment
15
- * directly, please use the {@link Collection.newAttachment | newAttachment()}
16
- * method of {@link Collection} instead.
21
+ * directly, please use the
22
+ * {@link Store.newAttachment | ditto.store.newAttachment()} method instead.
17
23
  */
18
24
  export class Attachment {
19
25
  /** @internal */
@@ -22,6 +28,16 @@ export class Attachment {
22
28
  /** @internal */
23
29
  readonly token: AttachmentToken
24
30
 
31
+ /** The attachment's ID. */
32
+ get id(): string {
33
+ return this.token.id
34
+ }
35
+
36
+ /** The attachment's size given as number of bytes. */
37
+ get len(): number | BigInt {
38
+ return this.token.len
39
+ }
40
+
25
41
  /** The attachment's metadata. */
26
42
  get metadata(): { [key: string]: string } {
27
43
  return this.token.metadata
@@ -30,24 +46,29 @@ export class Attachment {
30
46
  /**
31
47
  * Returns the attachment's data.
32
48
  */
33
- getData(): Promise<Uint8Array> {
49
+ data(): Promise<Uint8Array> {
34
50
  const ditto = this.ditto
35
51
  const dittoHandle = Bridge.ditto.handleFor(ditto)
36
52
  return this.ditto.deferCloseAsync(async () => {
37
- // if (Environment.isWebBuild) {
38
- // const attachmentHandle = Bridge.attachment.handleFor(this)
39
- // return await FFI.dittoGetCompleteAttachmentData(dittoHandle.deref(), attachmentHandle.deref())
40
- // }
41
-
42
- // if (Environment.isNodeBuild) {
43
- // const attachmentHandle = Bridge.attachment.handleFor(this)
44
- // const attachmentPath = FFI.dittoGetCompleteAttachmentPath(dittoHandle.deref(), attachmentHandle.deref())
45
- // const fs = require('fs').promises
46
- // return await fs.readFile(attachmentPath)
47
- // }
53
+
54
+
55
+ if (Environment.isReactNativeBuild) {
56
+ const attachmentHandle = Bridge.attachment.handleFor(this)
57
+ const attachmentPath = FFI.dittoGetCompleteAttachmentPath(dittoHandle.deref(), attachmentHandle.deref())
58
+ return await FFI.readFile(attachmentPath)
59
+ }
48
60
  })
49
61
  }
50
62
 
63
+ /**
64
+ * Returns the attachment's data.
65
+ *
66
+ * @deprecated Use `data()` instead.
67
+ */
68
+ getData(): Promise<Uint8Array> {
69
+ return this.data()
70
+ }
71
+
51
72
  /**
52
73
  * Copies the attachment to the specified file path. Node-only,
53
74
  * throws in the browser.
@@ -58,11 +79,14 @@ export class Attachment {
58
79
  const ditto = this.ditto
59
80
  const dittoHandle = Bridge.ditto.handleFor(ditto)
60
81
  return this.ditto.deferCloseAsync(async () => {
61
- if (Environment.isWebBuild) {
62
- throw new Error(`Can't copy attachment to path, not available when running in the browser.`)
63
- }
64
82
 
65
-
83
+ if (Environment.isReactNativeBuild) {
84
+ const attachmentHandle = Bridge.attachment.handleFor(this)
85
+ const attachmentPath = FFI.dittoGetCompleteAttachmentPath(dittoHandle.deref(), attachmentHandle.deref())
86
+ // If the file already exists, we fail. This is the same behavior as
87
+ // for the Swift/ObjC SDK.
88
+ return await FFI.copyFile(attachmentPath, path, ditto.persistenceDirectory)
89
+ }
66
90
  })
67
91
  }
68
92
 
@@ -72,3 +96,26 @@ export class Attachment {
72
96
  this.token = token
73
97
  }
74
98
  }
99
+
100
+ /**
101
+ * Validates the given attachment metadata. Metadata must be a flat object with
102
+ * string values.
103
+ *
104
+ * This should really happen in core to make sure we use the same validation
105
+ * logic across SDKs but we decided to postpone that for the next iteration on
106
+ * attachments.
107
+ *
108
+ * @throws {@link DittoError} 'store/failed-to-create-attachment'
109
+ * @internal
110
+ */
111
+ export function validateAttachmentMetadata(metadata: AttachmentMetadata): void {
112
+ if (typeof metadata !== 'object') {
113
+ throw new DittoError('store/failed-to-create-attachment', `Invalid attachment metadata: expected a value of type object but got ${typeof metadata}.`)
114
+ }
115
+
116
+ for (const key in metadata) {
117
+ if (typeof metadata[key] !== 'string') {
118
+ throw new DittoError('store/failed-to-create-attachment', `Invalid attachment metadata: metadata values must be strings but key '${key}' has a value of type ${typeof metadata[key]}.`)
119
+ }
120
+ }
121
+ }
@@ -46,11 +46,16 @@ export function augmentJSONValue(json, mutDoc, workingPath) {
46
46
  * Converts objects that may contain instances of classes of this SDK, i.e.
47
47
  * `DocumentID`, `Counter`, `Register` and `Attachment`, into plain JS objects
48
48
  * that can be passed to the FFI layer.
49
+ *
50
+ * WARNING: For attachments in the input, the output can contain `BigInt`
51
+ * values, which are not JSON-compatible.
52
+ *
53
+ * @throws {Error} If `jsObj` contains a non-finite float value.
49
54
  */
50
- export function desugarJSObject(jsObj: any, atRoot: boolean = false): any {
55
+ export function desugarJSObject(jsObj: any): any {
51
56
  if (jsObj && typeof jsObj === 'object') {
52
57
  if (Array.isArray(jsObj)) {
53
- return jsObj.map((v, idx) => desugarJSObject(v, false))
58
+ return jsObj.map((v, idx) => desugarJSObject(v))
54
59
  } else if (jsObj instanceof DocumentID) {
55
60
  return jsObj.value
56
61
  } else if (jsObj instanceof Counter) {
@@ -65,17 +70,19 @@ export function desugarJSObject(jsObj: any, atRoot: boolean = false): any {
65
70
  return registerJSON
66
71
  } else if (jsObj instanceof Attachment) {
67
72
  const attachmentJSON = {
68
- _id: jsObj.token.id,
69
- _len: jsObj.token.len,
73
+ _id: jsObj.token.idBytes,
74
+ _len: jsObj.token.len, // may be a BigInt
70
75
  _meta: jsObj.token.metadata,
71
76
  }
72
77
  attachmentJSON[FFI.DittoCRDTTypeKey] = FFI.DittoCRDTType.attachment
73
78
  return attachmentJSON
74
79
  } else {
80
+ // Create a copy to not mutate the original object
81
+ const jsObjJSON = {}
75
82
  for (const [key, value] of Object.entries(jsObj)) {
76
- jsObj[key] = desugarJSObject(value, false)
83
+ jsObjJSON[key] = desugarJSObject(value)
77
84
  }
78
- return jsObj
85
+ return jsObjJSON
79
86
  }
80
87
  } else {
81
88
  checkForUnsupportedValues(jsObj)
@@ -4,6 +4,7 @@
4
4
 
5
5
  import * as FFI from './ffi'
6
6
 
7
+ import { desugarJSObject } from './augment'
7
8
  import { Document, MutableDocument } from './document'
8
9
  import { UpdateResultsMap } from './update-results-map'
9
10
  import { CBOR } from './cbor'
@@ -39,6 +40,9 @@ export abstract class BasePendingCursorOperation implements PromiseLike<Document
39
40
  * Updates documents that match the query generated by the preceding function
40
41
  * chaining.
41
42
  *
43
+ * Document values must not be set to any non-finite numbers (`NaN`,
44
+ * `Infinity`, `-Infinity`).
45
+ *
42
46
  * @param closure A closure that gets called with all of the documents
43
47
  * matching the query. The documents are instances of {@link MutableDocument}
44
48
  * so you can call update-related functions on them.
@@ -64,18 +68,20 @@ export abstract class BasePendingCursorOperation implements PromiseLike<Document
64
68
  * Sorts the documents that match the query provided in the preceding
65
69
  * `find`-like function call.
66
70
  *
67
- * @param query The query specifies the logic to be used when sorting the
68
- * matching documents.
71
+ * Documents that are missing the field to sort by will appear at
72
+ * the beginning of the results when sorting in ascending order.
73
+ *
74
+ * @param query Name or path of the field to sort by.
69
75
  *
70
76
  * @param direction Specify whether you want the sorting order to be
71
- * `Ascending` or `Descending`.
77
+ * `ascending` or `descending`. Defaults to `ascending`.
72
78
  *
73
79
  * @return A cursor that you can chain further function calls and then either
74
80
  * get the matching documents immediately or get updates about them over time.
75
81
  */
76
- sort(propertyPath: string, direction: SortDirection = 'ascending'): BasePendingCursorOperation {
82
+ sort(query: string, direction: SortDirection = 'ascending'): BasePendingCursorOperation {
77
83
  this.orderBys.push({
78
- query: propertyPath,
84
+ query,
79
85
  direction: direction === 'ascending' ? 'Ascending' : 'Descending',
80
86
  })
81
87
  return this
@@ -97,6 +103,8 @@ export abstract class BasePendingCursorOperation implements PromiseLike<Document
97
103
  * get the matching documents immediately or get updates about them over time.
98
104
  */
99
105
  offset(offset: number): BasePendingCursorOperation {
106
+ // REFACTOR: factor out parameter validation.
107
+
100
108
  if (offset < 0) throw new Error(`Can't offset by '${offset}', offset must be >= 0`)
101
109
  if (!Number.isFinite(offset)) throw new Error(`Can't offset by '${offset}', offset must be a finite number`)
102
110
  if (Number.isNaN(offset)) throw new Error(`Can't offset by '${offset}', offset must be a valid number`)
@@ -118,6 +126,8 @@ export abstract class BasePendingCursorOperation implements PromiseLike<Document
118
126
  * get the matching documents immediately or get updates about them over time.
119
127
  */
120
128
  limit(limit: number): BasePendingCursorOperation {
129
+ // REFACTOR: factor out parameter validation.
130
+
121
131
  if (limit < -1) throw new Error(`Can't limit to '${limit}', limit must be >= -1 (where -1 means unlimited)`)
122
132
  if (!Number.isFinite(limit)) throw new Error(`Can't limit to '${limit}', limit must be a finite number`)
123
133
  if (Number.isNaN(limit)) throw new Error(`Can't limit to '${limit}', limit must be a valid number`)
@@ -215,7 +225,13 @@ export abstract class BasePendingCursorOperation implements PromiseLike<Document
215
225
  this.query = validateQuery(query)
216
226
  this.queryArgs = queryArgs ? Object.freeze({ ...queryArgs }) : null
217
227
  this.collection = collection
218
- this.queryArgsCBOR = queryArgs ? CBOR.encode(queryArgs) : null
228
+
229
+ if (queryArgs == null) {
230
+ this.queryArgsCBOR = null
231
+ } else {
232
+ const queryArgsJSON = desugarJSObject(queryArgs)
233
+ this.queryArgsCBOR = CBOR.encode(queryArgsJSON)
234
+ }
219
235
  }
220
236
 
221
237
  /** @internal */
@@ -42,6 +42,9 @@ export abstract class BasePendingIDSpecificOperation implements PromiseLike<Docu
42
42
  /**
43
43
  * Updates the document with the matching ID.
44
44
  *
45
+ * Document values must not be set to any non-finite numbers (`NaN`,
46
+ * `Infinity`, `-Infinity`).
47
+ *
45
48
  * @param closure A closure that gets called with the document matching the
46
49
  * ID. If found, the document is a {@link MutableDocument}, so you can call
47
50
  * update-related functions on it. If the document is not found then the value
@@ -5,11 +5,11 @@
5
5
  import * as FFI from './ffi'
6
6
 
7
7
  import { Logger } from './logger'
8
- import { QueryResult } from './query-result'
9
- import { QueryResultItem } from './query-result-item'
10
8
 
11
9
  import type { Document, MutableDocument } from './document'
12
10
  import type { Attachment } from './attachment'
11
+ import type { QueryResult } from './query-result'
12
+ import type { QueryResultItem } from './query-result-item'
13
13
 
14
14
  import type { StaticTCPClient } from './static-tcp-client'
15
15
  import type { WebsocketClient } from './websocket-client'
@@ -4,8 +4,6 @@
4
4
 
5
5
  import { CBOR as CBORRedux } from './@cbor-redux'
6
6
 
7
- import { DocumentID } from './document-id'
8
-
9
7
  /** @internal */
10
8
  export class CBOR {
11
9
  /** @internal */
@@ -20,16 +18,3 @@ export class CBOR {
20
18
  return CBORRedux.decode(arrayBuffer, reviver)
21
19
  }
22
20
  }
23
-
24
- /**
25
- * Custom replacer that converts `DocumentID` instances to their string
26
- * representation.
27
- *
28
- * @internal
29
- */
30
- export function documentIDReplacer(key: any, value: any): any {
31
- if (value instanceof DocumentID) {
32
- return value.toString()
33
- }
34
- return value
35
- }
@@ -10,6 +10,10 @@ import type { BasePendingCursorOperation } from './base-pending-cursor-operation
10
10
  import type { BasePendingIDSpecificOperation } from './base-pending-id-specific-operation'
11
11
 
12
12
  export type UpsertOptions = {
13
+ /**
14
+ * Specifies the desired strategy for inserting a document. The default
15
+ * strategy is `merge`. See {@link WriteStrategy} for more information.
16
+ */
13
17
  writeStrategy?: WriteStrategy
14
18
  }
15
19
 
@@ -55,13 +59,15 @@ export interface CollectionInterface {
55
59
  findByIDCBOR(idCBOR: Uint8Array): BasePendingIDSpecificOperation
56
60
 
57
61
  /**
58
- * Inserts a new document into the collection and returns its ID. If the
59
- * document already exists, the contents of both are merged by default. You
60
- * can change this by providing a different `writeStrategy` via `options`.
62
+ * Inserts a new document into the collection and returns its ID.
63
+ *
64
+ * If the document already exists, the contents of both are merged by default.
65
+ * You can change this by providing a different `writeStrategy` via `options`.
61
66
  *
62
- * @param value The content of the document to be inserted or updated.
63
- * @param options.writeStrategy Specifies the desired strategy for inserting a
64
- * document, defaults to `'merge'`.
67
+ * @param value The content of the document to be inserted or updated. Must
68
+ * not contain any non-finite numbers (NaN, Infinity, -Infinity).
69
+ * @param options Change defaults for the behavior of the operation, such as
70
+ * the write strategy.
65
71
  */
66
72
  upsert(value: DocumentValue, options: UpsertOptions): Promise<DocumentID>
67
73
  }
@@ -18,6 +18,7 @@ import { PendingIDSpecificOperation } from './pending-id-specific-operation'
18
18
  import { performAsyncToWorkaroundNonAsyncFFIAPI } from './internal'
19
19
  import { desugarJSObject } from './augment'
20
20
 
21
+ import type { TypedAttachmentToken } from './attachment-token'
21
22
  import type { Store } from './store'
22
23
  import type { DocumentValue } from './document'
23
24
  import type { DocumentIDValue } from './document-id'
@@ -94,7 +95,7 @@ export class Collection implements CollectionInterface {
94
95
  return ditto.deferCloseAsync(async () => {
95
96
  const writeStrategy = options.writeStrategy ?? 'merge'
96
97
 
97
- const documentValueJSON = desugarJSObject(value, true)
98
+ const documentValueJSON = desugarJSObject(value)
98
99
  const documentValueCBOR = CBOR.encode(documentValueJSON)
99
100
 
100
101
  const idCBOR = await performAsyncToWorkaroundNonAsyncFFIAPI(async () => {
@@ -130,6 +131,9 @@ export class Collection implements CollectionInterface {
130
131
  * attachment with or the raw data.
131
132
  *
132
133
  * @param metadata Metadata relating to the attachment.
134
+ *
135
+ * @deprecated Use {@link Store.newAttachment | ditto.store.newAttachment() }
136
+ * instead.
133
137
  */
134
138
  async newAttachment(pathOrData: string | Uint8Array, metadata: { [key: string]: string } = {}): Promise<Attachment> {
135
139
  const ditto = this.store.ditto
@@ -157,7 +161,7 @@ export class Collection implements CollectionInterface {
157
161
  const attachmentTokenJSON = { _id: id, _len: len, _meta: { ...metadata } }
158
162
  attachmentTokenJSON[FFI.DittoCRDTTypeKey] = FFI.DittoCRDTType.attachment
159
163
 
160
- const attachmentToken = new AttachmentToken(attachmentTokenJSON)
164
+ const attachmentToken = new AttachmentToken(attachmentTokenJSON as TypedAttachmentToken)
161
165
  const attachment = new Attachment(ditto, attachmentToken)
162
166
 
163
167
  return Bridge.attachment.bridge(handle, () => attachment)
@@ -187,6 +191,9 @@ export class Collection implements CollectionInterface {
187
191
  * @return An `AttachmentFetcher` object, which must be kept alive for the
188
192
  * fetch request to proceed and for you to be notified about the attachment's
189
193
  * fetch status changes.
194
+ *
195
+ * @deprecated Use
196
+ * {@link Store.fetchAttachment | ditto.store.fetchAttachment() } instead.
190
197
  */
191
198
  fetchAttachment(token: AttachmentToken, eventHandler?: (event: AttachmentFetchEvent) => void): AttachmentFetcher {
192
199
  if (token == null || !(token instanceof AttachmentToken)) {
@@ -217,9 +217,7 @@ export class Ditto {
217
217
 
218
218
  // Check if device name stays the same on sdk and core levels (#10729).
219
219
 
220
- if (Environment.isWebBuild) {
221
- this.deviceName = navigator.userAgent
222
- }
220
+
223
221
 
224
222
  if (Environment.isReactNativeBuild) {
225
223
  this.deviceName = FFI.getDeviceName() as string
@@ -227,16 +225,12 @@ export class Ditto {
227
225
 
228
226
  this.keepAlive = new KeepAlive()
229
227
 
230
- // NOTE: some behavior not implemented yet as compared to the ObjC/Swift
231
- // version:
232
- //
233
- // 1. Optional `identity` parameter falling back to a default one.
234
- //
235
- // 2. Optional siteID of the identity, falling back to a random one if
236
- // newly created or to the stored one if already persisted.
237
-
238
- const uninitializedDittoX = FFI.uninitializedDittoMake(this.persistenceDirectory)
239
-
228
+ // WORKAROUND: the login provider triggers the registered callback right
229
+ // when the auth client is being constructed. At this point, we don't have
230
+ // the auth client, which would be needed to perform a login, nor did we
231
+ // have a chance to create an authenticator. Therefore catch that first
232
+ // callback, store the seconds remaining and proceed with propagating
233
+ // it after all pieces are in place.
240
234
  let secondsRemainingUntilAuthenticationExpires: number | null = null
241
235
 
242
236
  const weakThis = new WeakRef(this)
@@ -275,7 +269,10 @@ export class Ditto {
275
269
  throw new Error(`Can't create Ditto, unsupported identity type: ${validIdentity}`)
276
270
  })()
277
271
 
278
- const dittoPointer = FFI.dittoMake(uninitializedDittoX, identityConfig)
272
+ // History tracking is an experimental feature that is not supported in the JS SDK.
273
+ const historyTracking = 'Disabled'
274
+
275
+ const dittoPointer = FFI.dittoMake(this.persistenceDirectory, identityConfig, historyTracking)
279
276
 
280
277
  FFI.dittoAuthClientSetValidityListener(dittoPointer, function (...args) {
281
278
  const ditto = weakThis.deref()
@@ -294,6 +291,10 @@ export class Ditto {
294
291
 
295
292
  Bridge.ditto.bridge(dittoPointer, this)
296
293
 
294
+ if (Environment.isReactNativeBuild) {
295
+ this.disableSyncWithV3()
296
+ }
297
+
297
298
  // IMPORTANT: Keeping the auth client around accumulates run-times and
298
299
  // resources which becomes a problem specifically in tests (where we use one
299
300
  // Ditto instance per test). We therefore keep it only if needed, i.e.
@@ -312,6 +313,8 @@ export class Ditto {
312
313
  if (strongThis.auth) {
313
314
  strongThis.auth['@ditto.authenticationExpiring'](secondsRemaining)
314
315
  } else {
316
+ // WORKAROUND: see description above where the
317
+ // secondsRemainingUntilAuthenticationExpires variable is declared.
315
318
  secondsRemainingUntilAuthenticationExpires = secondsRemaining
316
319
  }
317
320
  })
@@ -364,6 +367,8 @@ export class Ditto {
364
367
 
365
368
  disableDeadlockTimeoutWhenDebugging()
366
369
 
370
+ // WORKAROUND: see description above where the
371
+ // secondsRemainingUntilAuthenticationExpires variable is declared.
367
372
  if (secondsRemainingUntilAuthenticationExpires != null) {
368
373
  this.auth['@ditto.authenticationExpiring'](secondsRemainingUntilAuthenticationExpires)
369
374
  }
@@ -468,6 +473,7 @@ export class Ditto {
468
473
  validatedPath = path
469
474
  }
470
475
 
476
+
471
477
  if (Environment.isReactNativeBuild) {
472
478
  validatedPath = FFI.createDirectory(validatedPath) as string
473
479
  }
@@ -666,9 +672,6 @@ export class Ditto {
666
672
  * @throws {Error} if called in a React Native environment.
667
673
  */
668
674
  async disableSyncWithV3(): Promise<void> {
669
- if (Environment.isReactNativeBuild) {
670
- throw new Error('Disabling sync with V3 is not supported in a React Native environment.')
671
- }
672
675
  const dittoHandle = Bridge.ditto.handleFor(this)
673
676
  return this.deferCloseAsync(async () => {
674
677
  await FFI.dittoDisableSyncWithV3(dittoHandle.deref())
@@ -708,7 +711,11 @@ export class Ditto {
708
711
  // ignored because they are handled at the original call site.
709
712
  do {
710
713
  await Promise.allSettled(this.pendingOperations)
711
-
714
+ // REFACTOR: in theory, we could end up in an endless loop here if for
715
+ // some reason a resolved or rejected promise isn't removed from
716
+ // `pendingOperations`. AFAICS, this is not possible atm due to the
717
+ // way `deferClose` and `deferCloseAsync` is implemented. Would be
718
+ // great to rework this and make it more solid if possible.
712
719
  } while (this.pendingOperations.size > 0)
713
720
  this.deferCloseAllowed = false
714
721
 
@@ -975,5 +982,6 @@ export const disableDeadlockTimeoutWhenDebugging = () => {
975
982
  * @internal
976
983
  */
977
984
  const isDirectoryWritable = (directoryPath: string): boolean => {
985
+
978
986
  return true
979
987
  }
@@ -4,9 +4,10 @@
4
4
 
5
5
  import * as FFI from './ffi'
6
6
  import { CBOR } from './cbor'
7
+ import { mapFFIErrors } from './error'
7
8
 
8
9
  /** Represents a unique identifier for a {@link Document}. */
9
- export type DocumentIDValue = any
10
+ export type DocumentIDValue = any // REFACTOR: get rid of any.
10
11
 
11
12
  /** Represents a unique identifier for a {@link Document}. */
12
13
  export class DocumentID {
@@ -113,23 +114,26 @@ export class DocumentID {
113
114
  }
114
115
 
115
116
  /**
116
- * Returns a byte representation of the document ID value as base64 string.
117
+ * Returns the base64-encoded CBOR representation of this document ID.
118
+ *
119
+ * @deprecated
117
120
  */
118
121
  toBase64String(): string {
119
- const bytes = this['@ditto.cbor']
120
- return btoa(String.fromCharCode.apply(null, bytes))
122
+ return mapFFIErrors(() => FFI.base64encode(this['@ditto.cbor'], 'Padded'))
121
123
  }
122
124
 
123
125
  /**
124
126
  * Returns a query compatible string representation of the receiver.
125
127
  *
126
- * The returned string can be used directly in queries that you use with
127
- * other Ditto functions. For example you could create a query that was like
128
- * this:
128
+ * The returned string can be used directly in queries that you use with other
129
+ * Ditto functions. For example you could create a query that was like this:
129
130
  *
130
131
  * ``` TypeScript
131
132
  * collection.find(`_id == ${documentID.toQueryCompatibleString()}`)
132
133
  * ```
134
+ *
135
+ * @deprecated use document IDs in queries by embedding them in the query
136
+ * arguments parameter.
133
137
  */
134
138
  toQueryCompatibleString(): string {
135
139
  return FFI.documentIDQueryCompatible(this['@ditto.cbor'], 'WithQuotes')