velocious 1.0.476 → 1.0.478

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 (56) hide show
  1. package/README.md +1 -0
  2. package/build/configuration-types.js +3 -0
  3. package/build/configuration.js +24 -0
  4. package/build/frontend-model-controller.js +368 -11
  5. package/build/frontend-models/base.js +5 -1
  6. package/build/frontend-models/resource-definition.js +20 -3
  7. package/build/http-server/client/websocket-session.js +93 -0
  8. package/build/routes/hooks/frontend-model-command-route-hook.js +16 -0
  9. package/build/src/configuration-types.d.ts +15 -0
  10. package/build/src/configuration-types.d.ts.map +1 -1
  11. package/build/src/configuration-types.js +4 -1
  12. package/build/src/configuration.d.ts +13 -0
  13. package/build/src/configuration.d.ts.map +1 -1
  14. package/build/src/configuration.js +23 -1
  15. package/build/src/frontend-model-controller.d.ts +96 -2
  16. package/build/src/frontend-model-controller.d.ts.map +1 -1
  17. package/build/src/frontend-model-controller.js +319 -12
  18. package/build/src/frontend-models/base.d.ts +5 -0
  19. package/build/src/frontend-models/base.d.ts.map +1 -1
  20. package/build/src/frontend-models/base.js +6 -2
  21. package/build/src/frontend-models/resource-definition.d.ts.map +1 -1
  22. package/build/src/frontend-models/resource-definition.js +20 -4
  23. package/build/src/http-server/client/websocket-session.d.ts +39 -0
  24. package/build/src/http-server/client/websocket-session.d.ts.map +1 -1
  25. package/build/src/http-server/client/websocket-session.js +85 -1
  26. package/build/src/routes/hooks/frontend-model-command-route-hook.d.ts.map +1 -1
  27. package/build/src/routes/hooks/frontend-model-command-route-hook.js +15 -1
  28. package/build/src/sync/conflict-strategy.d.ts +83 -0
  29. package/build/src/sync/conflict-strategy.d.ts.map +1 -0
  30. package/build/src/sync/conflict-strategy.js +215 -0
  31. package/build/src/sync/peer-mutation-bundle.d.ts +97 -0
  32. package/build/src/sync/peer-mutation-bundle.d.ts.map +1 -0
  33. package/build/src/sync/peer-mutation-bundle.js +215 -0
  34. package/build/src/sync/server-change-feed.d.ts +265 -0
  35. package/build/src/sync/server-change-feed.d.ts.map +1 -0
  36. package/build/src/sync/server-change-feed.js +475 -0
  37. package/build/src/sync/sync-envelope-replay-service.d.ts +213 -0
  38. package/build/src/sync/sync-envelope-replay-service.d.ts.map +1 -0
  39. package/build/src/sync/sync-envelope-replay-service.js +229 -0
  40. package/build/sync/conflict-strategy.js +225 -0
  41. package/build/sync/peer-mutation-bundle.js +232 -0
  42. package/build/sync/server-change-feed.js +524 -0
  43. package/build/sync/sync-envelope-replay-service.js +263 -0
  44. package/build/tsconfig.tsbuildinfo +1 -1
  45. package/package.json +1 -1
  46. package/src/configuration-types.js +3 -0
  47. package/src/configuration.js +24 -0
  48. package/src/frontend-model-controller.js +368 -11
  49. package/src/frontend-models/base.js +5 -1
  50. package/src/frontend-models/resource-definition.js +20 -3
  51. package/src/http-server/client/websocket-session.js +93 -0
  52. package/src/routes/hooks/frontend-model-command-route-hook.js +16 -0
  53. package/src/sync/conflict-strategy.js +225 -0
  54. package/src/sync/peer-mutation-bundle.js +232 -0
  55. package/src/sync/server-change-feed.js +524 -0
  56. package/src/sync/sync-envelope-replay-service.js +263 -0
@@ -0,0 +1,263 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * Replays client sync envelopes through project supplied authentication,
5
+ * authorization, application, and persistence hooks.
6
+ *
7
+ * This is intentionally transport/model agnostic: Velocious owns the generic
8
+ * replay loop, normalization, stale-client comparison, and per-sync result
9
+ * shape while each app owns its token lookup, model handlers, and
10
+ * domain authorization rules.
11
+ */
12
+ export default class SyncEnvelopeReplayService {
13
+ /**
14
+ * Creates a sync envelope replay service.
15
+ * @param {object} [args] - Constructor arguments.
16
+ * @param {{debug?: (...args: Array<unknown>) => void, warn?: (...args: Array<unknown>) => void}} [args.logger] - Logger used for normalization warnings.
17
+ */
18
+ constructor(args = {}) {
19
+ this.logger = args.logger || console
20
+ }
21
+
22
+ /**
23
+ * Replays a sync batch.
24
+ * @param {Record<string, ?>} params - Request params carrying authentication and syncs.
25
+ * @returns {Promise<{syncs: Array<Record<string, ?>>, status?: string, errorCode?: string, errorMessage?: string}>} Replay response.
26
+ */
27
+ async replay(params) {
28
+ const actorResult = await this.authenticateReplay(params)
29
+
30
+ if (!actorResult.authenticated) {
31
+ return {
32
+ syncs: [],
33
+ status: "error",
34
+ errorCode: actorResult.errorCode,
35
+ errorMessage: actorResult.errorMessage
36
+ }
37
+ }
38
+
39
+ const syncResponses = []
40
+ const context = await this.buildReplayContext({actor: actorResult.actor, params})
41
+
42
+ for (const rawSync of this.replaySyncs(params)) {
43
+ const normalizedResult = this.normalizeReplaySync(rawSync)
44
+
45
+ if (!normalizedResult.ok) {
46
+ syncResponses.push(normalizedResult.response)
47
+ continue
48
+ }
49
+
50
+ const mutation = normalizedResult.mutation
51
+ const accessResult = await this.authorizeReplayMutation({actor: actorResult.actor, context, mutation})
52
+
53
+ if (!accessResult.allowed) {
54
+ syncResponses.push({
55
+ id: mutation.id,
56
+ syncState: "failed",
57
+ reason: accessResult.reason || "access-denied"
58
+ })
59
+ continue
60
+ }
61
+
62
+ const existingSync = await this.findExistingReplaySync({actor: actorResult.actor, context, mutation})
63
+ const shouldApply = await this.shouldApplyReplayMutation({actor: actorResult.actor, context, existingSync, mutation})
64
+ const applyResult = shouldApply
65
+ ? await this.applyReplayMutation({actor: actorResult.actor, context, existingSync, mutation})
66
+ : await this.skippedReplayMutation({actor: actorResult.actor, context, existingSync, mutation})
67
+
68
+ await this.persistReplayMutation({actor: actorResult.actor, context, existingSync, applyResult, mutation, shouldApply})
69
+ await this.afterReplayMutation({actor: actorResult.actor, context, existingSync, applyResult, mutation, shouldApply})
70
+
71
+ syncResponses.push({id: mutation.id, syncState: "successful"})
72
+ }
73
+
74
+ return {syncs: syncResponses}
75
+ }
76
+
77
+ /**
78
+ * Authenticates the sync batch actor.
79
+ * @param {Record<string, ?>} _params - Request params.
80
+ * @returns {Promise<{authenticated: true, actor: ?} | {authenticated: false, errorCode: string, errorMessage: string}>} Auth result.
81
+ */
82
+ async authenticateReplay(_params) {
83
+ throw new Error("SyncEnvelopeReplayService.authenticateReplay must be implemented")
84
+ }
85
+
86
+ /**
87
+ * Builds per-batch mutable context for caches shared across sync items.
88
+ * @param {{actor: ?, params: Record<string, ?>}} _args - Actor and request params.
89
+ * @returns {Promise<Record<string, ?>>} Replay context.
90
+ */
91
+ async buildReplayContext(_args) {
92
+ return {}
93
+ }
94
+
95
+ /**
96
+ * Returns raw sync entries from request params.
97
+ * @param {Record<string, ?>} params - Request params.
98
+ * @returns {Array<?>} Raw sync entries.
99
+ */
100
+ replaySyncs(params) {
101
+ return Array.isArray(params.syncs) ? params.syncs : []
102
+ }
103
+
104
+ /**
105
+ * Normalizes one sync entry.
106
+ * @param {?} rawSync - Raw sync entry.
107
+ * @returns {{ok: true, mutation: import("./sync-envelope-replay-service.js").SyncReplayMutation} | {ok: false, response: Record<string, ?>}} Normalized mutation or failed response.
108
+ */
109
+ normalizeReplaySync(rawSync) {
110
+ if (!rawSync || typeof rawSync !== "object" || Array.isArray(rawSync)) {
111
+ return {ok: false, response: {id: undefined, syncState: "failed", reason: "invalid-sync"}}
112
+ }
113
+
114
+ const sync = /** @type {Record<string, ?>} */ (rawSync)
115
+ const {clientUpdatedAt, data, id, resourceId, resourceType, syncType} = sync
116
+
117
+ if (typeof resourceType !== "string" || resourceType.length < 1 || resourceId === undefined || resourceId === null || typeof syncType !== "string" || syncType.length < 1) {
118
+ return {ok: false, response: {id, syncState: "failed", reason: "invalid-resource-id"}}
119
+ }
120
+
121
+ const resourceIdString = String(resourceId)
122
+ let clientUpdatedAtDate = typeof clientUpdatedAt === "string" || clientUpdatedAt instanceof Date ? new Date(clientUpdatedAt) : new Date()
123
+
124
+ if (Number.isNaN(clientUpdatedAtDate.getTime())) clientUpdatedAtDate = new Date()
125
+
126
+ const normalizedDataResult = this.normalizeReplaySyncData({data, id, resourceId: resourceIdString, resourceType})
127
+
128
+ if (!normalizedDataResult.ok) return normalizedDataResult
129
+
130
+ return {
131
+ ok: true,
132
+ mutation: {
133
+ clientUpdatedAt: clientUpdatedAtDate,
134
+ data: normalizedDataResult.data,
135
+ id,
136
+ resourceId: resourceIdString,
137
+ resourceType,
138
+ serializedData: JSON.stringify(normalizedDataResult.data),
139
+ syncType
140
+ }
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Normalizes one sync data payload.
146
+ * @param {{data: ?, id: ?, resourceId: string, resourceType: string}} args - Sync payload normalization arguments.
147
+ * @returns {{ok: true, data: Record<string, ?>} | {ok: false, response: Record<string, ?>}} Normalized payload or failed response.
148
+ */
149
+ normalizeReplaySyncData({data, id, resourceId, resourceType}) {
150
+ if (data === undefined || data === null) return {ok: true, data: {}}
151
+
152
+ if (typeof data === "string") {
153
+ try {
154
+ const parsedData = JSON.parse(data)
155
+
156
+ if (!parsedData || typeof parsedData !== "object" || Array.isArray(parsedData)) return {ok: true, data: {}}
157
+
158
+ return {ok: true, data: /** @type {Record<string, ?>} */ (parsedData)}
159
+ } catch (error) {
160
+ this.logger.warn?.("Invalid sync data JSON", {error, id, resourceId, resourceType})
161
+ return {ok: false, response: {id, syncState: "failed", reason: "invalid-data"}}
162
+ }
163
+ }
164
+
165
+ if (typeof data !== "object" || Array.isArray(data)) return {ok: true, data: {}}
166
+
167
+ return {ok: true, data: JSON.parse(JSON.stringify(data))}
168
+ }
169
+
170
+ /**
171
+ * Authorizes one normalized mutation.
172
+ * @param {{actor: ?, context: Record<string, ?>, mutation: import("./sync-envelope-replay-service.js").SyncReplayMutation}} _args - Actor, batch context, and mutation.
173
+ * @returns {Promise<{allowed: boolean, reason?: string}>} Access result.
174
+ */
175
+ async authorizeReplayMutation(_args) {
176
+ return {allowed: true}
177
+ }
178
+
179
+ /**
180
+ * Loads the previously stored sync/change row for stale-client comparison.
181
+ * @param {{actor: ?, context: Record<string, ?>, mutation: import("./sync-envelope-replay-service.js").SyncReplayMutation}} _args - Actor, batch context, and mutation.
182
+ * @returns {Promise<?>} Existing sync row.
183
+ */
184
+ async findExistingReplaySync(_args) {
185
+ return null
186
+ }
187
+
188
+ /**
189
+ * Returns whether a normalized mutation should be applied to domain models.
190
+ * @param {{actor: ?, context: Record<string, ?>, existingSync: ?, mutation: import("./sync-envelope-replay-service.js").SyncReplayMutation}} args - Actor, batch context, existing sync row, and mutation.
191
+ * @returns {Promise<boolean>} Whether to apply the mutation.
192
+ */
193
+ async shouldApplyReplayMutation({existingSync, mutation}) {
194
+ const existingClientUpdatedAt = this.existingReplaySyncClientUpdatedAt(existingSync)
195
+
196
+ return !existingClientUpdatedAt || mutation.clientUpdatedAt > existingClientUpdatedAt
197
+ }
198
+
199
+ /**
200
+ * Resolves the client timestamp from an existing sync row.
201
+ * @param {?} existingSync - Existing sync row.
202
+ * @returns {Date | null} Existing client timestamp.
203
+ */
204
+ existingReplaySyncClientUpdatedAt(existingSync) {
205
+ if (!existingSync || typeof existingSync !== "object") return null
206
+
207
+ const syncRecord = /** @type {Record<string, ?>} */ (existingSync)
208
+ const value = typeof syncRecord.clientUpdatedAt === "function"
209
+ ? syncRecord.clientUpdatedAt()
210
+ : syncRecord.clientUpdatedAt
211
+
212
+ if (value instanceof Date) return value
213
+
214
+ if (typeof value !== "string") return null
215
+
216
+ const parsedValue = new Date(value)
217
+
218
+ return Number.isNaN(parsedValue.getTime()) ? null : parsedValue
219
+ }
220
+
221
+ /**
222
+ * Applies one normalized mutation to domain models.
223
+ * @param {{actor: ?, context: Record<string, ?>, existingSync: ?, mutation: import("./sync-envelope-replay-service.js").SyncReplayMutation}} _args - Actor, batch context, existing sync row, and mutation.
224
+ * @returns {Promise<?>} Project-specific apply result.
225
+ */
226
+ async applyReplayMutation(_args) {
227
+ return null
228
+ }
229
+
230
+ /**
231
+ * Resolves an apply result for stale mutations that should not touch domain models.
232
+ * @param {{actor: ?, context: Record<string, ?>, existingSync: ?, mutation: import("./sync-envelope-replay-service.js").SyncReplayMutation}} _args - Actor, batch context, existing sync row, and mutation.
233
+ * @returns {Promise<?>} Project-specific apply result.
234
+ */
235
+ async skippedReplayMutation(_args) {
236
+ return null
237
+ }
238
+
239
+ /**
240
+ * Persists one normalized mutation into the app sync/change store.
241
+ * @param {{actor: ?, context: Record<string, ?>, existingSync: ?, applyResult: ?, mutation: import("./sync-envelope-replay-service.js").SyncReplayMutation, shouldApply: boolean}} _args - Replay persistence arguments.
242
+ * @returns {Promise<void>}
243
+ */
244
+ async persistReplayMutation(_args) {}
245
+
246
+ /**
247
+ * Runs side effects after a successful mutation replay and persistence.
248
+ * @param {{actor: ?, context: Record<string, ?>, existingSync: ?, applyResult: ?, mutation: import("./sync-envelope-replay-service.js").SyncReplayMutation, shouldApply: boolean}} _args - Replay side-effect arguments.
249
+ * @returns {Promise<void>}
250
+ */
251
+ async afterReplayMutation(_args) {}
252
+ }
253
+
254
+ /**
255
+ * @typedef {object} SyncReplayMutation
256
+ * @property {Date} clientUpdatedAt - Client-side mutation timestamp.
257
+ * @property {Record<string, ?>} data - Parsed mutation payload.
258
+ * @property {?} id - Client sync row id for per-sync responses.
259
+ * @property {string} resourceId - Resource id as a string.
260
+ * @property {string} resourceType - Resource/model name.
261
+ * @property {string} serializedData - JSON serialized mutation payload.
262
+ * @property {string} syncType - Sync operation type.
263
+ */