@verdant-web/common 1.11.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 (241) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cjs/EventSubscriber.d.ts +17 -0
  3. package/dist/cjs/EventSubscriber.js +67 -0
  4. package/dist/cjs/EventSubscriber.js.map +1 -0
  5. package/dist/cjs/baseline.d.ts +5 -0
  6. package/dist/cjs/baseline.js +3 -0
  7. package/dist/cjs/baseline.js.map +1 -0
  8. package/dist/cjs/batching.d.ts +41 -0
  9. package/dist/cjs/batching.js +89 -0
  10. package/dist/cjs/batching.js.map +1 -0
  11. package/dist/cjs/files.d.ts +19 -0
  12. package/dist/cjs/files.js +19 -0
  13. package/dist/cjs/files.js.map +1 -0
  14. package/dist/cjs/files.test.d.ts +1 -0
  15. package/dist/cjs/files.test.js +81 -0
  16. package/dist/cjs/files.test.js.map +1 -0
  17. package/dist/cjs/index.d.ts +18 -0
  18. package/dist/cjs/index.js +36 -0
  19. package/dist/cjs/index.js.map +1 -0
  20. package/dist/cjs/indexes.d.ts +14 -0
  21. package/dist/cjs/indexes.js +116 -0
  22. package/dist/cjs/indexes.js.map +1 -0
  23. package/dist/cjs/indexes.test.d.ts +1 -0
  24. package/dist/cjs/indexes.test.js +128 -0
  25. package/dist/cjs/indexes.test.js.map +1 -0
  26. package/dist/cjs/migration.d.ts +98 -0
  27. package/dist/cjs/migration.js +128 -0
  28. package/dist/cjs/migration.js.map +1 -0
  29. package/dist/cjs/oids.d.ts +87 -0
  30. package/dist/cjs/oids.js +375 -0
  31. package/dist/cjs/oids.js.map +1 -0
  32. package/dist/cjs/oids.test.d.ts +1 -0
  33. package/dist/cjs/oids.test.js +282 -0
  34. package/dist/cjs/oids.test.js.map +1 -0
  35. package/dist/cjs/operation.d.ts +139 -0
  36. package/dist/cjs/operation.js +533 -0
  37. package/dist/cjs/operation.js.map +1 -0
  38. package/dist/cjs/operation.test.d.ts +1 -0
  39. package/dist/cjs/operation.test.js +827 -0
  40. package/dist/cjs/operation.test.js.map +1 -0
  41. package/dist/cjs/patch.d.ts +26 -0
  42. package/dist/cjs/patch.js +227 -0
  43. package/dist/cjs/patch.js.map +1 -0
  44. package/dist/cjs/presence.d.ts +27 -0
  45. package/dist/cjs/presence.js +3 -0
  46. package/dist/cjs/presence.js.map +1 -0
  47. package/dist/cjs/protocol.d.ts +145 -0
  48. package/dist/cjs/protocol.js +4 -0
  49. package/dist/cjs/protocol.js.map +1 -0
  50. package/dist/cjs/refs.d.ts +5 -0
  51. package/dist/cjs/refs.js +22 -0
  52. package/dist/cjs/refs.js.map +1 -0
  53. package/dist/cjs/replica.d.ts +28 -0
  54. package/dist/cjs/replica.js +29 -0
  55. package/dist/cjs/replica.js.map +1 -0
  56. package/dist/cjs/schema/defaults.d.ts +4 -0
  57. package/dist/cjs/schema/defaults.js +72 -0
  58. package/dist/cjs/schema/defaults.js.map +1 -0
  59. package/dist/cjs/schema/defaults.test.d.ts +1 -0
  60. package/dist/cjs/schema/defaults.test.js +21 -0
  61. package/dist/cjs/schema/defaults.test.js.map +1 -0
  62. package/dist/cjs/schema/index.d.ts +8 -0
  63. package/dist/cjs/schema/index.js +29 -0
  64. package/dist/cjs/schema/index.js.map +1 -0
  65. package/dist/cjs/schema/indexFilters.d.ts +6 -0
  66. package/dist/cjs/schema/indexFilters.js +31 -0
  67. package/dist/cjs/schema/indexFilters.js.map +1 -0
  68. package/dist/cjs/schema/types/collection.d.ts +51 -0
  69. package/dist/cjs/schema/types/collection.js +3 -0
  70. package/dist/cjs/schema/types/collection.js.map +1 -0
  71. package/dist/cjs/schema/types/compounds.d.ts +12 -0
  72. package/dist/cjs/schema/types/compounds.js +3 -0
  73. package/dist/cjs/schema/types/compounds.js.map +1 -0
  74. package/dist/cjs/schema/types/fields.d.ts +59 -0
  75. package/dist/cjs/schema/types/fields.js +3 -0
  76. package/dist/cjs/schema/types/fields.js.map +1 -0
  77. package/dist/cjs/schema/types/filters.d.ts +29 -0
  78. package/dist/cjs/schema/types/filters.js +3 -0
  79. package/dist/cjs/schema/types/filters.js.map +1 -0
  80. package/dist/cjs/schema/types/shapes.d.ts +21 -0
  81. package/dist/cjs/schema/types/shapes.js +3 -0
  82. package/dist/cjs/schema/types/shapes.js.map +1 -0
  83. package/dist/cjs/schema/types/synthetics.d.ts +28 -0
  84. package/dist/cjs/schema/types/synthetics.js +3 -0
  85. package/dist/cjs/schema/types/synthetics.js.map +1 -0
  86. package/dist/cjs/schema/types.d.ts +18 -0
  87. package/dist/cjs/schema/types.js +23 -0
  88. package/dist/cjs/schema/types.js.map +1 -0
  89. package/dist/cjs/timestamp.d.ts +48 -0
  90. package/dist/cjs/timestamp.js +244 -0
  91. package/dist/cjs/timestamp.js.map +1 -0
  92. package/dist/cjs/timestamp.test.d.ts +1 -0
  93. package/dist/cjs/timestamp.test.js +94 -0
  94. package/dist/cjs/timestamp.test.js.map +1 -0
  95. package/dist/cjs/undo.d.ts +3 -0
  96. package/dist/cjs/undo.js +199 -0
  97. package/dist/cjs/undo.js.map +1 -0
  98. package/dist/cjs/utils.d.ts +15 -0
  99. package/dist/cjs/utils.js +128 -0
  100. package/dist/cjs/utils.js.map +1 -0
  101. package/dist/cjs/utils.test.d.ts +1 -0
  102. package/dist/cjs/utils.test.js +37 -0
  103. package/dist/cjs/utils.test.js.map +1 -0
  104. package/dist/esm/EventSubscriber.d.ts +17 -0
  105. package/dist/esm/EventSubscriber.js +63 -0
  106. package/dist/esm/EventSubscriber.js.map +1 -0
  107. package/dist/esm/baseline.d.ts +5 -0
  108. package/dist/esm/baseline.js +2 -0
  109. package/dist/esm/baseline.js.map +1 -0
  110. package/dist/esm/batching.d.ts +41 -0
  111. package/dist/esm/batching.js +84 -0
  112. package/dist/esm/batching.js.map +1 -0
  113. package/dist/esm/files.d.ts +19 -0
  114. package/dist/esm/files.js +13 -0
  115. package/dist/esm/files.js.map +1 -0
  116. package/dist/esm/files.test.d.ts +1 -0
  117. package/dist/esm/files.test.js +79 -0
  118. package/dist/esm/files.test.js.map +1 -0
  119. package/dist/esm/index.d.ts +18 -0
  120. package/dist/esm/index.js +16 -0
  121. package/dist/esm/index.js.map +1 -0
  122. package/dist/esm/indexes.d.ts +14 -0
  123. package/dist/esm/indexes.js +106 -0
  124. package/dist/esm/indexes.js.map +1 -0
  125. package/dist/esm/indexes.test.d.ts +1 -0
  126. package/dist/esm/indexes.test.js +126 -0
  127. package/dist/esm/indexes.test.js.map +1 -0
  128. package/dist/esm/migration.d.ts +98 -0
  129. package/dist/esm/migration.js +122 -0
  130. package/dist/esm/migration.js.map +1 -0
  131. package/dist/esm/oids.d.ts +87 -0
  132. package/dist/esm/oids.js +348 -0
  133. package/dist/esm/oids.js.map +1 -0
  134. package/dist/esm/oids.test.d.ts +1 -0
  135. package/dist/esm/oids.test.js +280 -0
  136. package/dist/esm/oids.test.js.map +1 -0
  137. package/dist/esm/operation.d.ts +139 -0
  138. package/dist/esm/operation.js +518 -0
  139. package/dist/esm/operation.js.map +1 -0
  140. package/dist/esm/operation.test.d.ts +1 -0
  141. package/dist/esm/operation.test.js +825 -0
  142. package/dist/esm/operation.test.js.map +1 -0
  143. package/dist/esm/patch.d.ts +26 -0
  144. package/dist/esm/patch.js +223 -0
  145. package/dist/esm/patch.js.map +1 -0
  146. package/dist/esm/presence.d.ts +27 -0
  147. package/dist/esm/presence.js +2 -0
  148. package/dist/esm/presence.js.map +1 -0
  149. package/dist/esm/protocol.d.ts +145 -0
  150. package/dist/esm/protocol.js +3 -0
  151. package/dist/esm/protocol.js.map +1 -0
  152. package/dist/esm/refs.d.ts +5 -0
  153. package/dist/esm/refs.js +17 -0
  154. package/dist/esm/refs.js.map +1 -0
  155. package/dist/esm/replica.d.ts +28 -0
  156. package/dist/esm/replica.js +26 -0
  157. package/dist/esm/replica.js.map +1 -0
  158. package/dist/esm/schema/defaults.d.ts +4 -0
  159. package/dist/esm/schema/defaults.js +66 -0
  160. package/dist/esm/schema/defaults.js.map +1 -0
  161. package/dist/esm/schema/defaults.test.d.ts +1 -0
  162. package/dist/esm/schema/defaults.test.js +19 -0
  163. package/dist/esm/schema/defaults.test.js.map +1 -0
  164. package/dist/esm/schema/index.d.ts +8 -0
  165. package/dist/esm/schema/index.js +10 -0
  166. package/dist/esm/schema/index.js.map +1 -0
  167. package/dist/esm/schema/indexFilters.d.ts +6 -0
  168. package/dist/esm/schema/indexFilters.js +23 -0
  169. package/dist/esm/schema/indexFilters.js.map +1 -0
  170. package/dist/esm/schema/types/collection.d.ts +51 -0
  171. package/dist/esm/schema/types/collection.js +2 -0
  172. package/dist/esm/schema/types/collection.js.map +1 -0
  173. package/dist/esm/schema/types/compounds.d.ts +12 -0
  174. package/dist/esm/schema/types/compounds.js +2 -0
  175. package/dist/esm/schema/types/compounds.js.map +1 -0
  176. package/dist/esm/schema/types/fields.d.ts +59 -0
  177. package/dist/esm/schema/types/fields.js +2 -0
  178. package/dist/esm/schema/types/fields.js.map +1 -0
  179. package/dist/esm/schema/types/filters.d.ts +29 -0
  180. package/dist/esm/schema/types/filters.js +2 -0
  181. package/dist/esm/schema/types/filters.js.map +1 -0
  182. package/dist/esm/schema/types/shapes.d.ts +21 -0
  183. package/dist/esm/schema/types/shapes.js +2 -0
  184. package/dist/esm/schema/types/shapes.js.map +1 -0
  185. package/dist/esm/schema/types/synthetics.d.ts +28 -0
  186. package/dist/esm/schema/types/synthetics.js +2 -0
  187. package/dist/esm/schema/types/synthetics.js.map +1 -0
  188. package/dist/esm/schema/types.d.ts +18 -0
  189. package/dist/esm/schema/types.js +7 -0
  190. package/dist/esm/schema/types.js.map +1 -0
  191. package/dist/esm/timestamp.d.ts +48 -0
  192. package/dist/esm/timestamp.js +229 -0
  193. package/dist/esm/timestamp.js.map +1 -0
  194. package/dist/esm/timestamp.test.d.ts +1 -0
  195. package/dist/esm/timestamp.test.js +92 -0
  196. package/dist/esm/timestamp.test.js.map +1 -0
  197. package/dist/esm/undo.d.ts +3 -0
  198. package/dist/esm/undo.js +195 -0
  199. package/dist/esm/undo.js.map +1 -0
  200. package/dist/esm/utils.d.ts +15 -0
  201. package/dist/esm/utils.js +114 -0
  202. package/dist/esm/utils.js.map +1 -0
  203. package/dist/esm/utils.test.d.ts +1 -0
  204. package/dist/esm/utils.test.js +35 -0
  205. package/dist/esm/utils.test.js.map +1 -0
  206. package/dist/tsconfig-cjs.tsbuildinfo +1 -0
  207. package/dist/tsconfig.tsbuildinfo +1 -0
  208. package/package.json +38 -0
  209. package/src/EventSubscriber.ts +81 -0
  210. package/src/baseline.ts +9 -0
  211. package/src/batching.ts +135 -0
  212. package/src/files.ts +34 -0
  213. package/src/index.ts +26 -0
  214. package/src/indexes.test.ts +158 -0
  215. package/src/indexes.ts +151 -0
  216. package/src/migration.ts +391 -0
  217. package/src/oids.test.ts +395 -0
  218. package/src/oids.ts +442 -0
  219. package/src/operation.test.ts +966 -0
  220. package/src/operation.ts +755 -0
  221. package/src/patch.ts +277 -0
  222. package/src/presence.ts +29 -0
  223. package/src/protocol.ts +184 -0
  224. package/src/refs.ts +16 -0
  225. package/src/replica.ts +29 -0
  226. package/src/schema/defaults.test.ts +19 -0
  227. package/src/schema/defaults.ts +72 -0
  228. package/src/schema/index.ts +28 -0
  229. package/src/schema/indexFilters.ts +49 -0
  230. package/src/schema/types/collection.ts +133 -0
  231. package/src/schema/types/compounds.ts +33 -0
  232. package/src/schema/types/fields.ts +85 -0
  233. package/src/schema/types/filters.ts +39 -0
  234. package/src/schema/types/shapes.ts +83 -0
  235. package/src/schema/types/synthetics.ts +45 -0
  236. package/src/schema/types.ts +28 -0
  237. package/src/timestamp.test.ts +113 -0
  238. package/src/timestamp.ts +284 -0
  239. package/src/undo.ts +205 -0
  240. package/src/utils.test.ts +38 -0
  241. package/src/utils.ts +137 -0
@@ -0,0 +1,284 @@
1
+ import cuid from 'cuid';
2
+ import { v4 } from 'uuid';
3
+
4
+ export interface TimestampProvider {
5
+ now(version: number): string;
6
+ update(remoteTimestamp: string): void;
7
+ zero(version: number): string;
8
+ getWallClockTime(timestamp: string): number;
9
+ }
10
+
11
+ const VERSION_BLOCK_LENGTH = 4;
12
+ const ENCODING_NUMBER_RADIX = 36;
13
+
14
+ export function encodeVersion(version: number | string): string {
15
+ return version
16
+ .toString(ENCODING_NUMBER_RADIX)
17
+ .padStart(VERSION_BLOCK_LENGTH, '0');
18
+ }
19
+
20
+ export function OLD_encodeVersion(version: number | string): string {
21
+ return version.toString().padStart(6, '0');
22
+ }
23
+
24
+ export class NaiveTimestampProvider implements TimestampProvider {
25
+ counter = 0;
26
+ now = (version: number | string) => {
27
+ return (
28
+ encodeVersion(version) + Date.now().toString() + '-' + this.counter++
29
+ );
30
+ };
31
+ update = () => {
32
+ this.counter = 0;
33
+ };
34
+ zero = (version: number | string) => {
35
+ return encodeVersion(version) + '0' + '-' + this.counter++;
36
+ };
37
+ getWallClockTime = (timestamp: string) => {
38
+ return parseInt(timestamp.slice(VERSION_BLOCK_LENGTH).split('-')[0], 10);
39
+ };
40
+ }
41
+
42
+ export class HybridLogicalClockTimestampProvider implements TimestampProvider {
43
+ private latest: HLCTimestamp = {
44
+ time: Date.now(),
45
+ counter: 0,
46
+ node: generateNodeId(),
47
+ };
48
+ private zeroCounter = 0;
49
+
50
+ now = (version: string | number) => {
51
+ this.latest = getHlcNow(this.latest);
52
+ return this.get(version, this.latest);
53
+ };
54
+ /**
55
+ * @deprecated - use now() instead and update to latest format
56
+ */
57
+ OLD_now = (version: string | number) => {
58
+ this.latest = getHlcNow(this.latest);
59
+ return OLD_encodeVersion(version) + OLD_serializeHlcTimestamp(this.latest);
60
+ };
61
+ /** Get the current timer state. Does not increment counter. */
62
+ timerState = () => {
63
+ return this.latest;
64
+ };
65
+ update = (remoteTimestamp: string) => {
66
+ // strip version from remote timestamp
67
+ const hlcString = remoteTimestamp.slice(VERSION_BLOCK_LENGTH);
68
+ this.latest = updateFromRemote(
69
+ this.latest,
70
+ deserializeHlcTimestamp(hlcString),
71
+ );
72
+ };
73
+ get = (version: string | number, raw: HLCTimestamp) => {
74
+ return encodeVersion(version) + serializeHlcTimestamp(raw);
75
+ };
76
+ zero = (version: string | number) => {
77
+ return (
78
+ encodeVersion(version) +
79
+ serializeHlcTimestamp({
80
+ time: 0,
81
+ // to keep zero timestamps unique, we use a counter here as well
82
+ counter: this.zeroCounter++,
83
+ node: this.latest.node,
84
+ })
85
+ );
86
+ };
87
+ getWallClockTime = (timestamp: string): number => {
88
+ return deserializeHlcTimestamp(timestamp.slice(VERSION_BLOCK_LENGTH)).time;
89
+ };
90
+ }
91
+
92
+ class ClockDriftError extends Error {
93
+ type: string;
94
+ constructor(...args: any[]) {
95
+ super();
96
+ this.type = 'ClockDriftError';
97
+ this.message = ['maximum clock drift exceeded'].concat(args).join(' ');
98
+ }
99
+ }
100
+
101
+ class OverflowError extends Error {
102
+ type: string;
103
+ constructor() {
104
+ super();
105
+ this.type = 'OverflowError';
106
+ this.message = 'timestamp counter overflow';
107
+ }
108
+ }
109
+
110
+ interface HLCTimestamp {
111
+ time: number;
112
+ counter: number;
113
+ node: string;
114
+ }
115
+
116
+ const COUNTER_BLOCK_LENGTH = 4;
117
+ const NODE_BLOCK_LENGTH = 7;
118
+ const MAX_CLOCK_DRIFT = 60 * 1000;
119
+ // 9 base36 characters should last until the year 5000
120
+ // when encoding a unix MS timestamp
121
+ const TIME_BLOCK_LENGTH = 9;
122
+
123
+ function generateNodeId() {
124
+ return cuid
125
+ .slug()
126
+ .padStart(NODE_BLOCK_LENGTH, '0')
127
+ .slice(0, NODE_BLOCK_LENGTH);
128
+ }
129
+
130
+ export function serializeHlcTimestamp(ts: HLCTimestamp): string {
131
+ // string representation of the time
132
+ const dateString = new Date(ts.time)
133
+ .getTime()
134
+ .toString(ENCODING_NUMBER_RADIX)
135
+ .padStart(TIME_BLOCK_LENGTH, '0');
136
+ // counter, 336, padded to 4 characters
137
+ const counter = ts.counter
138
+ .toString(ENCODING_NUMBER_RADIX)
139
+ .padStart(COUNTER_BLOCK_LENGTH, '0');
140
+ // node id padded to 16 characters
141
+ const node = ts.node.padStart(NODE_BLOCK_LENGTH, '0');
142
+ return `${dateString}${counter}${node}`;
143
+ }
144
+
145
+ function getHlcNow(prev: HLCTimestamp): HLCTimestamp {
146
+ const wallTime = Date.now();
147
+
148
+ // pick prev's time if it's later than our local device
149
+ const newWallTime = Math.max(prev.time, wallTime);
150
+ // reset counter if wall time changed
151
+ const newCounter = prev.time === newWallTime ? prev.counter + 1 : 0;
152
+
153
+ // check for drift
154
+ if (newWallTime - wallTime > MAX_CLOCK_DRIFT) {
155
+ throw new ClockDriftError(newWallTime, wallTime, MAX_CLOCK_DRIFT);
156
+ }
157
+ // check for counter overflow (max is 4 bytes)
158
+ if (newCounter > 65535) {
159
+ throw new OverflowError();
160
+ }
161
+
162
+ return {
163
+ time: newWallTime,
164
+ counter: newCounter,
165
+ node: prev.node,
166
+ };
167
+ }
168
+
169
+ function updateFromRemote(
170
+ local: HLCTimestamp,
171
+ remote: HLCTimestamp,
172
+ ): HLCTimestamp {
173
+ const wallTime = Date.now();
174
+ // pick remote's time if it's later than our local device
175
+ const newTime = Math.max(wallTime, Math.max(local.time, remote.time));
176
+
177
+ const maxCounter = Math.max(local.counter, remote.counter);
178
+ let newCounter;
179
+ // if all clocks are the same, increment the counter
180
+ if (local.time === newTime && remote.time === newTime) {
181
+ newCounter = maxCounter + 1;
182
+ }
183
+ // if local time is the same as new time, increment our local counter
184
+ else if (local.time === newTime) {
185
+ newCounter = local.counter + 1;
186
+ }
187
+ // if remote time is the same as new time, increment remote counter
188
+ else if (remote.time === newTime) {
189
+ newCounter = remote.counter + 1;
190
+ }
191
+ // otherwise, reset the counter
192
+ else {
193
+ newCounter = 0;
194
+ }
195
+
196
+ // check for drift
197
+ if (newTime - wallTime > MAX_CLOCK_DRIFT) {
198
+ throw new ClockDriftError(newTime, wallTime, MAX_CLOCK_DRIFT);
199
+ }
200
+ if (newCounter > 65535) {
201
+ throw new OverflowError();
202
+ }
203
+
204
+ return {
205
+ time: newTime,
206
+ counter: newCounter,
207
+ node: local.node,
208
+ };
209
+ }
210
+
211
+ export function deserializeHlcTimestamp(clock: string): HLCTimestamp {
212
+ const dateString = clock.slice(0, TIME_BLOCK_LENGTH);
213
+ const counter = clock.slice(
214
+ TIME_BLOCK_LENGTH,
215
+ TIME_BLOCK_LENGTH + COUNTER_BLOCK_LENGTH,
216
+ );
217
+ const node = clock.slice(TIME_BLOCK_LENGTH + COUNTER_BLOCK_LENGTH);
218
+ const time = parseInt(dateString, ENCODING_NUMBER_RADIX);
219
+ const counterNum = parseInt(counter, ENCODING_NUMBER_RADIX);
220
+
221
+ if (isNaN(time) || isNaN(counterNum)) {
222
+ throw new Error('invalid clock format');
223
+ }
224
+
225
+ return {
226
+ time,
227
+ counter: counterNum,
228
+ node,
229
+ };
230
+ }
231
+
232
+ function removeLeadingZeroes(str: string): string {
233
+ return str.replace(/^0+/, '');
234
+ }
235
+
236
+ /**
237
+ * Below, converters for old-style timestamps are defined.
238
+ * These are for migrating older clients to the new format.
239
+ */
240
+
241
+ /**
242
+ * Converts a timestamp from the old format to the new format.
243
+ */
244
+ export function convertOldHlcTimestamp(ts: string): string {
245
+ const versionBlock = ts.slice(0, 6);
246
+ // no jokes please
247
+ const clockBlock = ts.slice(6);
248
+ const deserialized = OLD_deserializeHlcTimestamp(clockBlock);
249
+
250
+ // version was base 10 and now is base 36
251
+ const versionNumber = parseInt(versionBlock, 10);
252
+ const encodedVersion = encodeVersion(versionNumber);
253
+ return encodedVersion + serializeHlcTimestamp(deserialized);
254
+ }
255
+
256
+ export function OLD_serializeHlcTimestamp(ts: HLCTimestamp): string {
257
+ // ISO string representation of the time
258
+ const dateString = new Date(ts.time).toISOString();
259
+ // counter, base16, padded to 4 characters
260
+ const counter = ts.counter.toString(16).toUpperCase().padStart(4, '0');
261
+ // node id padded to 16 characters
262
+ const node = ts.node.padStart(16, '0');
263
+ return `${dateString}-${counter}-${node}`;
264
+ }
265
+
266
+ export function OLD_deserializeHlcTimestamp(clock: string): HLCTimestamp {
267
+ // ISO string representation of the time
268
+ const dateString = clock.slice(0, 24);
269
+ const counter = clock.slice(25, 25 + 4);
270
+ const node = clock.slice(25 + 4 + 1);
271
+
272
+ const time = new Date(dateString).getTime();
273
+ const counterNum = parseInt(counter, 16);
274
+
275
+ if (isNaN(time) || isNaN(counterNum)) {
276
+ throw new Error('invalid clock format');
277
+ }
278
+
279
+ return {
280
+ time,
281
+ counter: counterNum,
282
+ node: node.slice(node.length - 7),
283
+ };
284
+ }
package/src/undo.ts ADDED
@@ -0,0 +1,205 @@
1
+ import { ObjectIdentifier } from './oids.js';
2
+ import { applyPatch, Operation } from './operation.js';
3
+ import { cloneDeep } from './utils.js';
4
+
5
+ export function getUndoOperations(
6
+ oid: ObjectIdentifier,
7
+ initial: any,
8
+ operations: Operation[],
9
+ getNow: () => string,
10
+ ): Operation[] {
11
+ if (initial === undefined) {
12
+ // if the initial state is nothing, then the undo is to delete everything.
13
+ // there's nothing else to worry about!
14
+ return [
15
+ {
16
+ oid,
17
+ timestamp: getNow(),
18
+ data: {
19
+ op: 'delete',
20
+ },
21
+ },
22
+ ];
23
+ }
24
+ // otherwise, traverse the operations one by one,
25
+ // applying them, and using the prior state to determine
26
+ // what the undo operation is.
27
+ let state = cloneDeep(initial);
28
+ const undoOperations: Operation[] = [];
29
+ for (const operation of operations) {
30
+ const undo = getUndoOperation(oid, state, operation, getNow);
31
+ undoOperations.unshift(...undo);
32
+ applyPatch(state, operation.data);
33
+ }
34
+ return undoOperations;
35
+ }
36
+
37
+ function getUndoOperation(
38
+ oid: ObjectIdentifier,
39
+ initial: any,
40
+ operation: Operation,
41
+ getNow: () => string,
42
+ ): Operation[] {
43
+ const data = operation.data;
44
+ switch (data.op) {
45
+ case 'set':
46
+ case 'remove':
47
+ return [
48
+ {
49
+ oid,
50
+ timestamp: getNow(),
51
+ data: {
52
+ op: 'set',
53
+ name: data.name,
54
+ value: initial[data.name],
55
+ },
56
+ },
57
+ ];
58
+ case 'list-insert':
59
+ return [
60
+ {
61
+ oid,
62
+ timestamp: getNow(),
63
+ data: {
64
+ op: 'list-delete',
65
+ index: data.index,
66
+ count: 1,
67
+ },
68
+ },
69
+ ];
70
+ case 'list-delete':
71
+ return [
72
+ {
73
+ oid,
74
+ timestamp: getNow(),
75
+ data: {
76
+ op: 'list-insert',
77
+ index: data.index,
78
+ values: initial.slice(data.index, data.count),
79
+ },
80
+ },
81
+ ];
82
+ case 'list-move-by-ref':
83
+ return [
84
+ {
85
+ oid,
86
+ timestamp: getNow(),
87
+ data: {
88
+ op: 'list-move-by-ref',
89
+ value: data.value,
90
+ index: initial.indexOf(data.value),
91
+ },
92
+ },
93
+ ];
94
+ case 'list-move-by-index':
95
+ return [
96
+ {
97
+ oid,
98
+ timestamp: getNow(),
99
+ data: {
100
+ op: 'list-move-by-index',
101
+ from: data.to,
102
+ to: data.from,
103
+ },
104
+ },
105
+ ];
106
+ case 'delete':
107
+ return [
108
+ {
109
+ oid,
110
+ timestamp: getNow(),
111
+ data: {
112
+ op: 'initialize',
113
+ value: initial,
114
+ },
115
+ },
116
+ ];
117
+ case 'list-push':
118
+ return [
119
+ {
120
+ oid,
121
+ timestamp: getNow(),
122
+ data: {
123
+ op: 'list-remove',
124
+ value: data.value,
125
+ // best heuristic here - remove the last instance of the item.
126
+ only: 'last',
127
+ },
128
+ },
129
+ ];
130
+ case 'list-remove':
131
+ if (data.only === 'last') {
132
+ const index = initial.lastIndexOf(data.value);
133
+ return [
134
+ {
135
+ oid,
136
+ timestamp: getNow(),
137
+ data: {
138
+ op: 'list-insert',
139
+ index,
140
+ values: [data.value],
141
+ },
142
+ },
143
+ ];
144
+ } else if (data.only === 'first') {
145
+ const index = initial.indexOf(data.value);
146
+ return [
147
+ {
148
+ oid,
149
+ timestamp: getNow(),
150
+ data: {
151
+ op: 'list-insert',
152
+ index,
153
+ values: [data.value],
154
+ },
155
+ },
156
+ ];
157
+ } else {
158
+ // find all instances of value and restore them at their
159
+ // original index
160
+ const indexesOfValue = [];
161
+ let index = initial.indexOf(data.value);
162
+ while (index !== -1) {
163
+ indexesOfValue.push(index);
164
+ index = initial.indexOf(data.value, index + 1);
165
+ }
166
+ return indexesOfValue.map((index) => ({
167
+ oid,
168
+ timestamp: getNow(),
169
+ data: {
170
+ op: 'list-insert',
171
+ index,
172
+ value: data.value,
173
+ },
174
+ }));
175
+ }
176
+ case 'list-add':
177
+ // this one is kind of ambiguous. in theory the set may have
178
+ // already included the value and so no change happened. but
179
+ // basically we infer the intent is to remove what was meant
180
+ // to be added by this change.
181
+ return [
182
+ {
183
+ oid,
184
+ timestamp: getNow(),
185
+ data: {
186
+ op: 'list-remove',
187
+ value: data.value,
188
+ only: 'last',
189
+ },
190
+ },
191
+ ];
192
+ case 'initialize':
193
+ return [
194
+ {
195
+ oid,
196
+ timestamp: getNow(),
197
+ data: {
198
+ op: 'delete',
199
+ },
200
+ },
201
+ ];
202
+ default:
203
+ throw new Error(`Cannot undo operation type: ${(data as any).op}`);
204
+ }
205
+ }
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { assignOid, assignOidsToAllSubObjects, getOid } from './oids.js';
3
+ import { cloneDeep, getSortedIndex } from './utils.js';
4
+
5
+ describe('utils', () => {
6
+ describe('get sorted index', () => {
7
+ it('finds the right insertion index', () => {
8
+ function compare(a: number, b: number) {
9
+ return a - b;
10
+ }
11
+ expect(getSortedIndex([1, 2, 3, 4, 5], 6, compare)).toBe(5);
12
+ expect(getSortedIndex([1, 2, 3, 4, 5], 0, compare)).toBe(0);
13
+ expect(getSortedIndex([1, 2, 3, 4, 5], -1, compare)).toBe(0);
14
+ expect(getSortedIndex([1, 2, 3, 4, 5], 20, compare)).toBe(5);
15
+ });
16
+ });
17
+
18
+ describe('clone deep', () => {
19
+ it('preserves OIDs', () => {
20
+ const obj = {
21
+ foo: 'bar',
22
+ qux: [
23
+ {
24
+ corge: true,
25
+ },
26
+ ],
27
+ };
28
+ assignOid(obj, 'test/a');
29
+ assignOidsToAllSubObjects(obj);
30
+
31
+ const cloned = cloneDeep(obj);
32
+
33
+ expect(getOid(cloned)).toEqual(getOid(obj));
34
+ expect(getOid(cloned.qux)).toEqual(getOid(obj.qux));
35
+ expect(getOid(cloned.qux[0])).toEqual(getOid(obj.qux[0]));
36
+ });
37
+ });
38
+ });
package/src/utils.ts ADDED
@@ -0,0 +1,137 @@
1
+ import { v4 } from 'uuid';
2
+ import { assignOid, maybeGetOid } from './oids.js';
3
+
4
+ export function take<T extends object, Keys extends keyof T>(
5
+ obj: T,
6
+ keys: Keys[],
7
+ ): Pick<T, Keys> {
8
+ const result: any = {};
9
+ for (const key of keys) {
10
+ result[key] = obj[key];
11
+ }
12
+ return result;
13
+ }
14
+
15
+ export function omit<T extends object, Keys extends keyof T>(
16
+ obj: T,
17
+ keys: Keys[],
18
+ ): Omit<T, Keys> {
19
+ const result: any = {};
20
+ for (const key of Object.keys(obj)) {
21
+ if (!keys.includes(key as Keys)) {
22
+ result[key] = (obj as any)[key];
23
+ }
24
+ }
25
+ return result;
26
+ }
27
+
28
+ export function getSortedIndex<T>(
29
+ array: T[],
30
+ insert: T,
31
+ compare: (a: T, b: T) => number,
32
+ ) {
33
+ let low = 0;
34
+ let high = array.length;
35
+ while (low < high) {
36
+ const mid = (low + high) >>> 1;
37
+ const cmp = compare(array[mid], insert);
38
+ if (cmp < 0) {
39
+ low = mid + 1;
40
+ } else {
41
+ high = mid;
42
+ }
43
+ }
44
+ return low;
45
+ }
46
+
47
+ function orderedReplacer(_: any, v: any) {
48
+ if (typeof v !== 'object' || v === null || Array.isArray(v)) {
49
+ return v;
50
+ }
51
+ return Object.fromEntries(
52
+ Object.entries(v).sort(([ka], [kb]) => (ka < kb ? -1 : ka > kb ? 1 : 0)),
53
+ );
54
+ }
55
+ /**
56
+ * Consistently stringifies an object regardless
57
+ * of key insertion order
58
+ */
59
+ export function stableStringify(obj: any) {
60
+ return JSON.stringify(obj, orderedReplacer);
61
+ }
62
+
63
+ export function cloneDeep<T>(obj: T): T {
64
+ if (isObject(obj)) {
65
+ const oid = maybeGetOid(obj);
66
+ let clone: any;
67
+ if (Array.isArray(obj)) {
68
+ clone = obj.map(cloneDeep) as T;
69
+ } else {
70
+ clone = {};
71
+ for (const [key, value] of Object.entries(obj as any)) {
72
+ clone[key] = cloneDeep(value);
73
+ }
74
+ }
75
+ if (oid) {
76
+ assignOid(clone, oid);
77
+ }
78
+ return clone;
79
+ }
80
+ return obj;
81
+ }
82
+
83
+ // TODO: better hash
84
+ export function hashObject(obj: any) {
85
+ return stableStringify(obj);
86
+ }
87
+
88
+ export function isObject(obj: any) {
89
+ return obj && typeof obj === 'object';
90
+ }
91
+
92
+ export function roughSizeOfObject(object: any) {
93
+ var objectList = [];
94
+ var stack = [object];
95
+ var bytes = 0;
96
+
97
+ while (stack.length) {
98
+ var value = stack.pop();
99
+
100
+ if (typeof value === 'boolean') {
101
+ bytes += 4;
102
+ } else if (typeof value === 'string') {
103
+ bytes += value.length * 2;
104
+ } else if (typeof value === 'number') {
105
+ bytes += 8;
106
+ } else if (typeof value === 'object' && objectList.indexOf(value) === -1) {
107
+ objectList.push(value);
108
+
109
+ for (var i in value) {
110
+ stack.push(value[i]);
111
+ }
112
+ }
113
+ }
114
+ return bytes;
115
+ }
116
+
117
+ export function assert(
118
+ condition: any,
119
+ message: string = 'assertion failed',
120
+ ): asserts condition {
121
+ if (!condition) {
122
+ throw new Error(message);
123
+ }
124
+ }
125
+
126
+ export function generateId(length = 16) {
127
+ return v4().replace('-', '').slice(0, length);
128
+ }
129
+
130
+ export function findLastIndex<T>(array: T[], predicate: (item: T) => boolean) {
131
+ for (let i = array.length - 1; i >= 0; i--) {
132
+ if (predicate(array[i])) {
133
+ return i;
134
+ }
135
+ }
136
+ return -1;
137
+ }