@stratasync/core 0.2.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.
- package/README.md +83 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/model/base-model.d.ts +82 -0
- package/dist/model/base-model.d.ts.map +1 -0
- package/dist/model/base-model.js +219 -0
- package/dist/model/base-model.js.map +1 -0
- package/dist/model/cached-promise.d.ts +15 -0
- package/dist/model/cached-promise.d.ts.map +1 -0
- package/dist/model/cached-promise.js +50 -0
- package/dist/model/cached-promise.js.map +1 -0
- package/dist/model/collection.d.ts +33 -0
- package/dist/model/collection.d.ts.map +1 -0
- package/dist/model/collection.js +181 -0
- package/dist/model/collection.js.map +1 -0
- package/dist/model/hydration.d.ts +13 -0
- package/dist/model/hydration.d.ts.map +1 -0
- package/dist/model/hydration.js +2 -0
- package/dist/model/hydration.js.map +1 -0
- package/dist/model/observability.d.ts +23 -0
- package/dist/model/observability.d.ts.map +1 -0
- package/dist/model/observability.js +162 -0
- package/dist/model/observability.js.map +1 -0
- package/dist/reactivity/adapter.d.ts +145 -0
- package/dist/reactivity/adapter.d.ts.map +1 -0
- package/dist/reactivity/adapter.js +95 -0
- package/dist/reactivity/adapter.js.map +1 -0
- package/dist/schema/decorators.d.ts +8 -0
- package/dist/schema/decorators.d.ts.map +1 -0
- package/dist/schema/decorators.js +117 -0
- package/dist/schema/decorators.js.map +1 -0
- package/dist/schema/hash.d.ts +6 -0
- package/dist/schema/hash.d.ts.map +1 -0
- package/dist/schema/hash.js +76 -0
- package/dist/schema/hash.js.map +1 -0
- package/dist/schema/normalize.d.ts +5 -0
- package/dist/schema/normalize.d.ts.map +1 -0
- package/dist/schema/normalize.js +194 -0
- package/dist/schema/normalize.js.map +1 -0
- package/dist/schema/registry.d.ts +147 -0
- package/dist/schema/registry.d.ts.map +1 -0
- package/dist/schema/registry.js +304 -0
- package/dist/schema/registry.js.map +1 -0
- package/dist/schema/types.d.ts +215 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/schema/types.js +2 -0
- package/dist/schema/types.js.map +1 -0
- package/dist/store/types.d.ts +14 -0
- package/dist/store/types.d.ts.map +1 -0
- package/dist/store/types.js +2 -0
- package/dist/store/types.js.map +1 -0
- package/dist/sync/delta-applier.d.ts +52 -0
- package/dist/sync/delta-applier.d.ts.map +1 -0
- package/dist/sync/delta-applier.js +110 -0
- package/dist/sync/delta-applier.js.map +1 -0
- package/dist/sync/rebase.d.ts +57 -0
- package/dist/sync/rebase.d.ts.map +1 -0
- package/dist/sync/rebase.js +155 -0
- package/dist/sync/rebase.js.map +1 -0
- package/dist/sync/sync-id.d.ts +17 -0
- package/dist/sync/sync-id.d.ts.map +1 -0
- package/dist/sync/sync-id.js +26 -0
- package/dist/sync/sync-id.js.map +1 -0
- package/dist/sync/types.d.ts +152 -0
- package/dist/sync/types.d.ts.map +1 -0
- package/dist/sync/types.js +2 -0
- package/dist/sync/types.js.map +1 -0
- package/dist/transaction/archive.d.ts +16 -0
- package/dist/transaction/archive.d.ts.map +1 -0
- package/dist/transaction/archive.js +23 -0
- package/dist/transaction/archive.js.map +1 -0
- package/dist/transaction/create.d.ts +31 -0
- package/dist/transaction/create.d.ts.map +1 -0
- package/dist/transaction/create.js +121 -0
- package/dist/transaction/create.js.map +1 -0
- package/dist/transaction/types.d.ts +86 -0
- package/dist/transaction/types.d.ts.map +1 -0
- package/dist/transaction/types.js +2 -0
- package/dist/transaction/types.js.map +1 -0
- package/dist/utils/assign.d.ts +9 -0
- package/dist/utils/assign.d.ts.map +1 -0
- package/dist/utils/assign.js +20 -0
- package/dist/utils/assign.js.map +1 -0
- package/dist/utils/idempotency.d.ts +16 -0
- package/dist/utils/idempotency.d.ts.map +1 -0
- package/dist/utils/idempotency.js +39 -0
- package/dist/utils/idempotency.js.map +1 -0
- package/package.json +37 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { createArchivePayload, createUnarchivePatch, readArchivedAt, } from "../transaction/archive.js";
|
|
2
|
+
import { maxSyncId, ZERO_SYNC_ID } from "./sync-id.js";
|
|
3
|
+
/**
|
|
4
|
+
* Merges data into an existing row and writes it back.
|
|
5
|
+
*/
|
|
6
|
+
const mergeAndPut = async (target, modelName, modelId, data, overrides) => {
|
|
7
|
+
const existing = await target.get(modelName, modelId);
|
|
8
|
+
await target.put(modelName, modelId, {
|
|
9
|
+
...existing,
|
|
10
|
+
...data,
|
|
11
|
+
...overrides,
|
|
12
|
+
});
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Applies a single sync action to the target
|
|
16
|
+
*/
|
|
17
|
+
const applySingleAction = async (action, target, options) => {
|
|
18
|
+
const { modelName, modelId, data } = action;
|
|
19
|
+
switch (action.action) {
|
|
20
|
+
case "I": {
|
|
21
|
+
await target.put(modelName, modelId, data);
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
case "U": {
|
|
25
|
+
if (options.mergeUpdates) {
|
|
26
|
+
await target.patch(modelName, modelId, data);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
const existing = await target.get(modelName, modelId);
|
|
30
|
+
await target.put(modelName, modelId, existing ? { ...existing, ...data } : data);
|
|
31
|
+
}
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
case "D": {
|
|
35
|
+
await target.delete(modelName, modelId);
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
case "A": {
|
|
39
|
+
await mergeAndPut(target, modelName, modelId, data, createArchivePayload(readArchivedAt(data)));
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
case "V": {
|
|
43
|
+
await mergeAndPut(target, modelName, modelId, data, createUnarchivePatch());
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
default: {
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Updates the result counters based on action type
|
|
53
|
+
*/
|
|
54
|
+
const updateResult = (result, action) => {
|
|
55
|
+
const actionSyncId = action.id;
|
|
56
|
+
result.lastSyncId = maxSyncId(result.lastSyncId, actionSyncId);
|
|
57
|
+
switch (action.action) {
|
|
58
|
+
case "I": {
|
|
59
|
+
result.inserts += 1;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
case "U": {
|
|
63
|
+
result.updates += 1;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
case "D": {
|
|
67
|
+
result.deletes += 1;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
case "A": {
|
|
71
|
+
result.archives += 1;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
case "V": {
|
|
75
|
+
result.unarchives += 1;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
default: {
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
export const applyDeltas = async (packet, target, registry, options = {}) => {
|
|
84
|
+
const result = {
|
|
85
|
+
archives: 0,
|
|
86
|
+
deletes: 0,
|
|
87
|
+
inserts: 0,
|
|
88
|
+
lastSyncId: ZERO_SYNC_ID,
|
|
89
|
+
skipped: 0,
|
|
90
|
+
unarchives: 0,
|
|
91
|
+
updates: 0,
|
|
92
|
+
};
|
|
93
|
+
for (const action of packet.actions) {
|
|
94
|
+
// Skip actions from our own client (we already applied them optimistically)
|
|
95
|
+
const actionSyncId = action.id;
|
|
96
|
+
if (options.clientId && action.clientId === options.clientId) {
|
|
97
|
+
result.skipped += 1;
|
|
98
|
+
result.lastSyncId = maxSyncId(result.lastSyncId, actionSyncId);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
// Verify model exists in schema
|
|
102
|
+
if (!registry.hasModel(action.modelName)) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
await applySingleAction(action, target, options);
|
|
106
|
+
updateResult(result, action);
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
};
|
|
110
|
+
//# sourceMappingURL=delta-applier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delta-applier.js","sourceRoot":"","sources":["../../src/sync/delta-applier.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Transaction } from "../transaction/types.js";
|
|
2
|
+
import type { SyncAction } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Result of rebasing pending transactions against server deltas
|
|
5
|
+
*/
|
|
6
|
+
export interface RebaseResult {
|
|
7
|
+
/** Transactions that should remain pending */
|
|
8
|
+
pending: Transaction[];
|
|
9
|
+
/** Transactions that were confirmed by the server */
|
|
10
|
+
confirmed: Transaction[];
|
|
11
|
+
/** Transactions that conflict with server changes */
|
|
12
|
+
conflicts: RebaseConflict[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A conflict between a pending transaction and a server change
|
|
16
|
+
*/
|
|
17
|
+
export interface RebaseConflict {
|
|
18
|
+
/** The local pending transaction */
|
|
19
|
+
localTransaction: Transaction;
|
|
20
|
+
/** The conflicting server action */
|
|
21
|
+
serverAction: SyncAction;
|
|
22
|
+
/** Type of conflict */
|
|
23
|
+
conflictType: ConflictType;
|
|
24
|
+
/** Suggested resolution */
|
|
25
|
+
resolution: ConflictResolution;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Types of conflicts that can occur
|
|
29
|
+
*/
|
|
30
|
+
type ConflictType = "update-update" | "update-delete" | "delete-update" | "insert-insert";
|
|
31
|
+
/**
|
|
32
|
+
* Possible conflict resolutions
|
|
33
|
+
*/
|
|
34
|
+
export type ConflictResolution = "server-wins" | "client-wins" | "merge" | "manual";
|
|
35
|
+
/**
|
|
36
|
+
* Options for rebase operation
|
|
37
|
+
*/
|
|
38
|
+
export interface RebaseOptions {
|
|
39
|
+
/** Client ID to identify own transactions */
|
|
40
|
+
clientId: string;
|
|
41
|
+
/** Default conflict resolution strategy */
|
|
42
|
+
defaultResolution?: ConflictResolution;
|
|
43
|
+
/** Field-level conflict detection */
|
|
44
|
+
fieldLevelConflicts?: boolean;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Rebases pending transactions against server deltas
|
|
48
|
+
*
|
|
49
|
+
* This is the core algorithm for handling concurrent edits:
|
|
50
|
+
* 1. For each server action, check if we have a pending transaction for the same model/id
|
|
51
|
+
* 2. If the server action is our own transaction (matched by clientTxId), mark as confirmed
|
|
52
|
+
* 3. If the server action conflicts with our pending transaction, detect the conflict type
|
|
53
|
+
* 4. Apply the appropriate resolution strategy
|
|
54
|
+
*/
|
|
55
|
+
export declare const rebaseTransactions: (pending: Transaction[], serverActions: SyncAction[], options: RebaseOptions) => RebaseResult;
|
|
56
|
+
export {};
|
|
57
|
+
//# sourceMappingURL=rebase.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rebase.d.ts","sourceRoot":"","sources":["../../src/sync/rebase.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,8CAA8C;IAC9C,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,qDAAqD;IACrD,SAAS,EAAE,WAAW,EAAE,CAAC;IACzB,qDAAqD;IACrD,SAAS,EAAE,cAAc,EAAE,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,oCAAoC;IACpC,gBAAgB,EAAE,WAAW,CAAC;IAC9B,oCAAoC;IACpC,YAAY,EAAE,UAAU,CAAC;IACzB,uBAAuB;IACvB,YAAY,EAAE,YAAY,CAAC;IAC3B,2BAA2B;IAC3B,UAAU,EAAE,kBAAkB,CAAC;CAChC;AAED;;GAEG;AACH,KAAK,YAAY,GAEb,eAAe,GAEf,eAAe,GAEf,eAAe,GAEf,eAAe,CAAC;AAEpB;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAE1B,aAAa,GAEb,aAAa,GAEb,OAAO,GAEP,QAAQ,CAAC;AAEb;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,6CAA6C;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,iBAAiB,CAAC,EAAE,kBAAkB,CAAC;IACvC,qCAAqC;IACrC,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAoHD;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAC7B,SAAS,WAAW,EAAE,EACtB,eAAe,UAAU,EAAE,EAC3B,SAAS,aAAa,KACrB,YA2DF,CAAC"}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes Archive (A) and Unarchive (V) actions to Update (U)
|
|
3
|
+
* since they are semantically update-like operations.
|
|
4
|
+
*/
|
|
5
|
+
const normalizeAction = (action) => {
|
|
6
|
+
switch (action) {
|
|
7
|
+
case "I": {
|
|
8
|
+
return "I";
|
|
9
|
+
}
|
|
10
|
+
case "U":
|
|
11
|
+
case "A":
|
|
12
|
+
case "V": {
|
|
13
|
+
return "U";
|
|
14
|
+
}
|
|
15
|
+
case "D": {
|
|
16
|
+
return "D";
|
|
17
|
+
}
|
|
18
|
+
default: {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const getLocalConflictFields = (tx) => {
|
|
24
|
+
if (tx.action === "A" || tx.action === "V") {
|
|
25
|
+
return ["archivedAt"];
|
|
26
|
+
}
|
|
27
|
+
return Object.keys(tx.payload);
|
|
28
|
+
};
|
|
29
|
+
const getServerConflictFields = (action) => {
|
|
30
|
+
if (action.action === "A" || action.action === "V") {
|
|
31
|
+
const dataKeys = Object.keys(action.data).filter((key) => key !== "archivedAt");
|
|
32
|
+
return ["archivedAt", ...dataKeys];
|
|
33
|
+
}
|
|
34
|
+
return Object.keys(action.data);
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Determines the resolution strategy for a conflict
|
|
38
|
+
*/
|
|
39
|
+
const resolveConflict = (conflictType, options) => {
|
|
40
|
+
if (options.defaultResolution) {
|
|
41
|
+
return options.defaultResolution;
|
|
42
|
+
}
|
|
43
|
+
// insert-insert is rare (likely a bug) and requires manual resolution.
|
|
44
|
+
// All other conflict types default to server-wins (last-write-wins).
|
|
45
|
+
if (conflictType === "insert-insert") {
|
|
46
|
+
return "manual";
|
|
47
|
+
}
|
|
48
|
+
return "server-wins";
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Detects if there's a conflict between a local transaction and server action
|
|
52
|
+
*/
|
|
53
|
+
const detectConflict = (tx, action, options) => {
|
|
54
|
+
const normalizedServer = normalizeAction(action.action);
|
|
55
|
+
const normalizedLocal = normalizeAction(tx.action);
|
|
56
|
+
if (!(normalizedServer && normalizedLocal)) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
// modelName and modelId match is guaranteed by the caller's key-based lookup
|
|
60
|
+
let conflictType;
|
|
61
|
+
if (normalizedLocal === "I" && normalizedServer === "I") {
|
|
62
|
+
conflictType = "insert-insert";
|
|
63
|
+
}
|
|
64
|
+
else if (normalizedLocal === "D" && normalizedServer === "U") {
|
|
65
|
+
conflictType = "delete-update";
|
|
66
|
+
}
|
|
67
|
+
else if (normalizedLocal === "U" && normalizedServer === "D") {
|
|
68
|
+
conflictType = "update-delete";
|
|
69
|
+
}
|
|
70
|
+
else if (normalizedLocal === "U" && normalizedServer === "U") {
|
|
71
|
+
// Check for field-level conflicts
|
|
72
|
+
if (options.fieldLevelConflicts) {
|
|
73
|
+
const localFields = getLocalConflictFields(tx);
|
|
74
|
+
const serverFields = getServerConflictFields(action);
|
|
75
|
+
const overlap = localFields.some((f) => serverFields.includes(f));
|
|
76
|
+
if (!overlap) {
|
|
77
|
+
// No overlapping fields, can merge
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
conflictType = "update-update";
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
// No conflict for other combinations
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
// Determine resolution
|
|
88
|
+
const resolution = resolveConflict(conflictType, options);
|
|
89
|
+
return {
|
|
90
|
+
conflictType,
|
|
91
|
+
localTransaction: tx,
|
|
92
|
+
resolution,
|
|
93
|
+
serverAction: action,
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Rebases pending transactions against server deltas
|
|
98
|
+
*
|
|
99
|
+
* This is the core algorithm for handling concurrent edits:
|
|
100
|
+
* 1. For each server action, check if we have a pending transaction for the same model/id
|
|
101
|
+
* 2. If the server action is our own transaction (matched by clientTxId), mark as confirmed
|
|
102
|
+
* 3. If the server action conflicts with our pending transaction, detect the conflict type
|
|
103
|
+
* 4. Apply the appropriate resolution strategy
|
|
104
|
+
*/
|
|
105
|
+
export const rebaseTransactions = (pending, serverActions, options) => {
|
|
106
|
+
const result = {
|
|
107
|
+
confirmed: [],
|
|
108
|
+
conflicts: [],
|
|
109
|
+
pending: [],
|
|
110
|
+
};
|
|
111
|
+
// Index pending transactions by model+id for fast lookup
|
|
112
|
+
const pendingByKey = new Map();
|
|
113
|
+
for (const tx of pending) {
|
|
114
|
+
const key = `${tx.modelName}:${tx.modelId}`;
|
|
115
|
+
const existing = pendingByKey.get(key) ?? [];
|
|
116
|
+
existing.push(tx);
|
|
117
|
+
pendingByKey.set(key, existing);
|
|
118
|
+
}
|
|
119
|
+
// Track which transactions have been processed
|
|
120
|
+
const processed = new Set();
|
|
121
|
+
// Process each server action
|
|
122
|
+
for (const action of serverActions) {
|
|
123
|
+
const key = `${action.modelName}:${action.modelId}`;
|
|
124
|
+
const relatedTxs = pendingByKey.get(key) ?? [];
|
|
125
|
+
for (const tx of relatedTxs) {
|
|
126
|
+
if (processed.has(tx.clientTxId)) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
// Check if this server action is our own transaction
|
|
130
|
+
if (action.clientTxId === tx.clientTxId &&
|
|
131
|
+
action.clientId === options.clientId) {
|
|
132
|
+
result.confirmed.push(tx);
|
|
133
|
+
processed.add(tx.clientTxId);
|
|
134
|
+
// Our own echo — don't conflict-check remaining pending txs against it.
|
|
135
|
+
// Subsequent pending mutations (e.g. undo) were created on top of this
|
|
136
|
+
// change and should not be rolled back by its server confirmation.
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
// Check for conflicts
|
|
140
|
+
const conflict = detectConflict(tx, action, options);
|
|
141
|
+
if (conflict) {
|
|
142
|
+
result.conflicts.push(conflict);
|
|
143
|
+
processed.add(tx.clientTxId);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Remaining unprocessed transactions stay pending
|
|
148
|
+
for (const tx of pending) {
|
|
149
|
+
if (!processed.has(tx.clientTxId)) {
|
|
150
|
+
result.pending.push(tx);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return result;
|
|
154
|
+
};
|
|
155
|
+
//# sourceMappingURL=rebase.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rebase.js","sourceRoot":"","sources":["../../src/sync/rebase.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync ID type — string on the wire, represents a monotonically increasing integer.
|
|
3
|
+
* Using string avoids precision loss for values exceeding Number.MAX_SAFE_INTEGER.
|
|
4
|
+
*/
|
|
5
|
+
export type SyncId = string;
|
|
6
|
+
/** Zero sync ID constant */
|
|
7
|
+
export declare const ZERO_SYNC_ID: SyncId;
|
|
8
|
+
/**
|
|
9
|
+
* Compares two string-encoded sync IDs numerically.
|
|
10
|
+
* Returns negative if a < b, 0 if equal, positive if a > b.
|
|
11
|
+
*/
|
|
12
|
+
export declare const compareSyncId: (a: SyncId, b: SyncId) => number;
|
|
13
|
+
/** Returns the larger of two sync IDs */
|
|
14
|
+
export declare const maxSyncId: (a: SyncId, b: SyncId) => SyncId;
|
|
15
|
+
/** Checks if sync ID a is greater than sync ID b */
|
|
16
|
+
export declare const isSyncIdGreaterThan: (a: SyncId, b: SyncId) => boolean;
|
|
17
|
+
//# sourceMappingURL=sync-id.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-id.d.ts","sourceRoot":"","sources":["../../src/sync/sync-id.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,MAAM,MAAM,GAAG,MAAM,CAAC;AAE5B,4BAA4B;AAC5B,eAAO,MAAM,YAAY,EAAE,MAAY,CAAC;AAIxC;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAI,GAAG,MAAM,EAAE,GAAG,MAAM,KAAG,MAcpD,CAAC;AAEF,yCAAyC;AACzC,eAAO,MAAM,SAAS,GAAI,GAAG,MAAM,EAAE,GAAG,MAAM,KAAG,MACf,CAAC;AAEnC,oDAAoD;AACpD,eAAO,MAAM,mBAAmB,GAAI,GAAG,MAAM,EAAE,GAAG,MAAM,KAAG,OAClC,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Zero sync ID constant */
|
|
2
|
+
export const ZERO_SYNC_ID = "0";
|
|
3
|
+
const LEADING_ZEROS = /^0+/;
|
|
4
|
+
/**
|
|
5
|
+
* Compares two string-encoded sync IDs numerically.
|
|
6
|
+
* Returns negative if a < b, 0 if equal, positive if a > b.
|
|
7
|
+
*/
|
|
8
|
+
export const compareSyncId = (a, b) => {
|
|
9
|
+
const aStripped = a.replace(LEADING_ZEROS, "") || "0";
|
|
10
|
+
const bStripped = b.replace(LEADING_ZEROS, "") || "0";
|
|
11
|
+
if (aStripped.length !== bStripped.length) {
|
|
12
|
+
return aStripped.length - bStripped.length;
|
|
13
|
+
}
|
|
14
|
+
if (aStripped < bStripped) {
|
|
15
|
+
return -1;
|
|
16
|
+
}
|
|
17
|
+
if (aStripped > bStripped) {
|
|
18
|
+
return 1;
|
|
19
|
+
}
|
|
20
|
+
return 0;
|
|
21
|
+
};
|
|
22
|
+
/** Returns the larger of two sync IDs */
|
|
23
|
+
export const maxSyncId = (a, b) => compareSyncId(a, b) >= 0 ? a : b;
|
|
24
|
+
/** Checks if sync ID a is greater than sync ID b */
|
|
25
|
+
export const isSyncIdGreaterThan = (a, b) => compareSyncId(a, b) > 0;
|
|
26
|
+
//# sourceMappingURL=sync-id.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-id.js","sourceRoot":"","sources":["../../src/sync/sync-id.ts"],"names":[],"mappings":"AAMA,4BAA4B;AAC5B,MAAM,CAAC,MAAM,YAAY,GAAW,GAAG,CAAC;AAExC,MAAM,aAAa,GAAG,KAAK,CAAC;AAE5B;;;GAGG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAS,EAAE,CAAS,EAAU,EAAE;IAC5D,MAAM,SAAS,GAAG,CAAC,CAAC,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC;IACtD,MAAM,SAAS,GAAG,CAAC,CAAC,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC;IAEtD,IAAI,SAAS,CAAC,MAAM,KAAK,SAAS,CAAC,MAAM,EAAE,CAAC;QAC1C,OAAO,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,MAAM,CAAC;IAC7C,CAAC;IACD,IAAI,SAAS,GAAG,SAAS,EAAE,CAAC;QAC1B,OAAO,CAAC,CAAC,CAAC;IACZ,CAAC;IACD,IAAI,SAAS,GAAG,SAAS,EAAE,CAAC;QAC1B,OAAO,CAAC,CAAC;IACX,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC,CAAC;AAEF,yCAAyC;AACzC,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,CAAS,EAAE,CAAS,EAAU,EAAE,CACxD,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAEnC,oDAAoD;AACpD,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAS,EAAE,CAAS,EAAW,EAAE,CACnE,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { TransactionAction } from "../schema/types.js";
|
|
2
|
+
import type { SyncId } from "./sync-id.js";
|
|
3
|
+
/**
|
|
4
|
+
* Sync action types emitted by the server.
|
|
5
|
+
* Includes mutation actions plus sync-group / coverage signals.
|
|
6
|
+
*/
|
|
7
|
+
export type SyncActionType = TransactionAction | "C" | "G" | "S";
|
|
8
|
+
/**
|
|
9
|
+
* A sync action represents a single change from the server
|
|
10
|
+
*/
|
|
11
|
+
export interface SyncAction {
|
|
12
|
+
/** Monotonically increasing sync ID (string-encoded for BigInt safety) */
|
|
13
|
+
id: SyncId;
|
|
14
|
+
/** Name of the model that changed */
|
|
15
|
+
modelName: string;
|
|
16
|
+
/** ID of the model instance */
|
|
17
|
+
modelId: string;
|
|
18
|
+
/** Type of change */
|
|
19
|
+
action: SyncActionType;
|
|
20
|
+
/** Full or partial data for the model */
|
|
21
|
+
data: Record<string, unknown>;
|
|
22
|
+
/** Group ID for multi-tenancy filtering */
|
|
23
|
+
groupId?: string;
|
|
24
|
+
/** Group IDs for multi-tenancy filtering */
|
|
25
|
+
groups?: string[];
|
|
26
|
+
/** Client transaction ID if this was a client mutation */
|
|
27
|
+
clientTxId?: string;
|
|
28
|
+
/** Client ID that originated the change */
|
|
29
|
+
clientId?: string;
|
|
30
|
+
/** Timestamp when the change was created */
|
|
31
|
+
createdAt?: Date;
|
|
32
|
+
/** Class name marker (Done payloads include __class) */
|
|
33
|
+
__class?: "SyncAction";
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A packet of delta changes from the server
|
|
37
|
+
*/
|
|
38
|
+
export interface DeltaPacket {
|
|
39
|
+
/** Highest sync ID in this packet */
|
|
40
|
+
lastSyncId: SyncId;
|
|
41
|
+
/** Array of sync actions */
|
|
42
|
+
actions: SyncAction[];
|
|
43
|
+
/** Whether there are more deltas available */
|
|
44
|
+
hasMore?: boolean;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Bootstrap metadata for initial sync
|
|
48
|
+
*/
|
|
49
|
+
export interface BootstrapMetadata {
|
|
50
|
+
/** Latest sync ID at bootstrap completion (may be omitted for partial/batch payloads) */
|
|
51
|
+
lastSyncId?: SyncId;
|
|
52
|
+
/** Groups returned by the server for this user */
|
|
53
|
+
subscribedSyncGroups: string[];
|
|
54
|
+
/** Count of returned models (by model name) */
|
|
55
|
+
returnedModelsCount?: Record<string, number>;
|
|
56
|
+
/** Schema hash returned by server (if provided) */
|
|
57
|
+
schemaHash?: string;
|
|
58
|
+
/** Server database version (if provided) */
|
|
59
|
+
databaseVersion?: number;
|
|
60
|
+
/** Additional metadata fields (if provided) */
|
|
61
|
+
raw?: Record<string, unknown>;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* A row of model data during bootstrap
|
|
65
|
+
*/
|
|
66
|
+
export interface ModelRow {
|
|
67
|
+
/** Model name */
|
|
68
|
+
modelName: string;
|
|
69
|
+
/** Row data */
|
|
70
|
+
data: Record<string, unknown>;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* State of the sync client
|
|
74
|
+
*/
|
|
75
|
+
export type SyncClientState = "disconnected" | "connecting" | "bootstrapping" | "syncing" | "error";
|
|
76
|
+
/**
|
|
77
|
+
* Connection state for transport
|
|
78
|
+
*/
|
|
79
|
+
export type ConnectionState = "disconnected" | "connecting" | "connected" | "reconnecting" | "error";
|
|
80
|
+
/**
|
|
81
|
+
* Options for bootstrap operation
|
|
82
|
+
*/
|
|
83
|
+
export interface BootstrapOptions {
|
|
84
|
+
/** Bootstrap type (full or partial) */
|
|
85
|
+
type?: "full" | "partial";
|
|
86
|
+
/** Models to include in bootstrap */
|
|
87
|
+
onlyModels?: string[];
|
|
88
|
+
/** Schema hash for validation/caching (optional) */
|
|
89
|
+
schemaHash?: string;
|
|
90
|
+
/** First sync ID for partial bootstrap (optional) */
|
|
91
|
+
firstSyncId?: SyncId;
|
|
92
|
+
/** Sync groups to bootstrap (optional) */
|
|
93
|
+
syncGroups?: string[];
|
|
94
|
+
/** Skip sync packets during partial bootstrap (optional) */
|
|
95
|
+
noSyncPackets?: boolean;
|
|
96
|
+
/** Enable CDN caching (optional) */
|
|
97
|
+
useCFCaching?: boolean;
|
|
98
|
+
/** Disable cache (optional) */
|
|
99
|
+
noCache?: boolean;
|
|
100
|
+
/** Models hash for cache validation (optional) */
|
|
101
|
+
modelsHash?: string;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Options for delta subscription
|
|
105
|
+
*/
|
|
106
|
+
export interface SubscribeOptions {
|
|
107
|
+
/** Start after this sync ID */
|
|
108
|
+
afterSyncId: SyncId;
|
|
109
|
+
/** Groups to subscribe to */
|
|
110
|
+
groups: string[];
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Delta subscription handle
|
|
114
|
+
*/
|
|
115
|
+
export interface DeltaSubscription {
|
|
116
|
+
/** Async iterator of delta packets */
|
|
117
|
+
[Symbol.asyncIterator](): AsyncIterator<DeltaPacket>;
|
|
118
|
+
/** Unsubscribe and close connection */
|
|
119
|
+
unsubscribe(): void;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Batch load request for lazy-loaded models
|
|
123
|
+
*/
|
|
124
|
+
export type BatchRequest = {
|
|
125
|
+
/** Model name */
|
|
126
|
+
modelName: string;
|
|
127
|
+
/** Indexed key to filter by */
|
|
128
|
+
indexedKey: string;
|
|
129
|
+
/** Indexed key value */
|
|
130
|
+
keyValue: string;
|
|
131
|
+
/** Disallow groupId for indexed requests */
|
|
132
|
+
groupId?: never;
|
|
133
|
+
} | {
|
|
134
|
+
/** Model name */
|
|
135
|
+
modelName: string;
|
|
136
|
+
/** Sync group ID */
|
|
137
|
+
groupId: string;
|
|
138
|
+
/** Disallow indexed key filters for group requests */
|
|
139
|
+
indexedKey?: never;
|
|
140
|
+
/** Disallow indexed key values for group requests */
|
|
141
|
+
keyValue?: never;
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Batch load options for partial hydration
|
|
145
|
+
*/
|
|
146
|
+
export interface BatchLoadOptions {
|
|
147
|
+
/** First sync ID from full bootstrap */
|
|
148
|
+
firstSyncId: SyncId;
|
|
149
|
+
/** Batch requests to load */
|
|
150
|
+
requests: BatchRequest[];
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/sync/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/sync/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface ArchiveState extends Record<string, unknown> {
|
|
2
|
+
archivedAt?: number | null;
|
|
3
|
+
}
|
|
4
|
+
export interface ArchiveTransactionOptions {
|
|
5
|
+
original?: Record<string, unknown>;
|
|
6
|
+
archivedAt?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface UnarchiveTransactionOptions {
|
|
9
|
+
original?: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
export declare const readArchivedAt: (record: Record<string, unknown> | ArchiveState | undefined) => number | undefined;
|
|
12
|
+
export declare const captureArchiveState: (record: Record<string, unknown> | ArchiveState | undefined) => ArchiveState;
|
|
13
|
+
export declare const createArchivePayload: (archivedAt?: number) => ArchiveState;
|
|
14
|
+
export declare const createUnarchivePatch: () => ArchiveState;
|
|
15
|
+
export declare const createUnarchivePayload: () => Record<string, unknown>;
|
|
16
|
+
//# sourceMappingURL=archive.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive.d.ts","sourceRoot":"","sources":["../../src/transaction/archive.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAa,SAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAC3D,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAID,eAAO,MAAM,cAAc,GACzB,QAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,YAAY,GAAG,SAAS,KACzD,MAAM,GAAG,SAgBX,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAC9B,QAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,YAAY,GAAG,SAAS,KACzD,YAAgE,CAAC;AAEpE,eAAO,MAAM,oBAAoB,GAAI,aAAa,MAAM,KAAG,YAEzD,CAAC;AAEH,eAAO,MAAM,oBAAoB,QAAO,YAAsC,CAAC;AAE/E,eAAO,MAAM,sBAAsB,QAAO,MAAM,CAAC,MAAM,EAAE,OAAO,CAAS,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const ISO_ARCHIVE_TIMESTAMP_REGEX = /^\d{4}-\d{2}-\d{2}(?:[T ][0-9:.+Z-]+)?$/;
|
|
2
|
+
export const readArchivedAt = (record) => {
|
|
3
|
+
const archivedAt = record?.archivedAt;
|
|
4
|
+
if (typeof archivedAt === "number") {
|
|
5
|
+
return archivedAt;
|
|
6
|
+
}
|
|
7
|
+
if (typeof archivedAt === "string") {
|
|
8
|
+
const isIsoTimestamp = ISO_ARCHIVE_TIMESTAMP_REGEX.test(archivedAt);
|
|
9
|
+
if (!isIsoTimestamp) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
const parsed = Date.parse(archivedAt);
|
|
13
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
14
|
+
}
|
|
15
|
+
return undefined;
|
|
16
|
+
};
|
|
17
|
+
export const captureArchiveState = (record) => ({ archivedAt: readArchivedAt(record) ?? null });
|
|
18
|
+
export const createArchivePayload = (archivedAt) => ({
|
|
19
|
+
archivedAt: archivedAt ?? Date.now(),
|
|
20
|
+
});
|
|
21
|
+
export const createUnarchivePatch = () => ({ archivedAt: null });
|
|
22
|
+
export const createUnarchivePayload = () => ({});
|
|
23
|
+
//# sourceMappingURL=archive.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive.js","sourceRoot":"","sources":["../../src/transaction/archive.ts"],"names":[],"mappings":"AAaA,MAAM,2BAA2B,GAAG,yCAAyC,CAAC;AAE9E,MAAM,CAAC,MAAM,cAAc,GAAG,CAC5B,MAA0D,EACtC,EAAE;IACtB,MAAM,UAAU,GAAG,MAAM,EAAE,UAAU,CAAC;IACtC,IAAI,OAAO,UAAU,KAAK,QAAQ,EAAE,CAAC;QACnC,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,IAAI,OAAO,UAAU,KAAK,QAAQ,EAAE,CAAC;QACnC,MAAM,cAAc,GAAG,2BAA2B,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACpE,IAAI,CAAC,cAAc,EAAE,CAAC;YACpB,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QACtC,OAAO,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC;IACnD,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,mBAAmB,GAAG,CACjC,MAA0D,EAC5C,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,cAAc,CAAC,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;AAEpE,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,UAAmB,EAAgB,EAAE,CAAC,CAAC;IAC1E,UAAU,EAAE,UAAU,IAAI,IAAI,CAAC,GAAG,EAAE;CACrC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,oBAAoB,GAAG,GAAiB,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;AAE/E,MAAM,CAAC,MAAM,sBAAsB,GAAG,GAA4B,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ArchiveTransactionOptions, UnarchiveTransactionOptions } from "./archive.js";
|
|
2
|
+
import type { Transaction, TransactionBatch } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Creates a transaction batch from an array of transactions
|
|
5
|
+
*/
|
|
6
|
+
export declare const createTransactionBatch: (transactions: Transaction[]) => TransactionBatch;
|
|
7
|
+
/**
|
|
8
|
+
* Creates an INSERT transaction for a new model instance
|
|
9
|
+
*/
|
|
10
|
+
export declare const createInsertTransaction: (clientId: string, modelName: string, modelId: string, data: Record<string, unknown>) => Transaction;
|
|
11
|
+
/**
|
|
12
|
+
* Creates an UPDATE transaction for an existing model instance
|
|
13
|
+
*/
|
|
14
|
+
export declare const createUpdateTransaction: (clientId: string, modelName: string, modelId: string, changes: Record<string, unknown>, original: Record<string, unknown>) => Transaction;
|
|
15
|
+
/**
|
|
16
|
+
* Creates a DELETE transaction for removing a model instance
|
|
17
|
+
*/
|
|
18
|
+
export declare const createDeleteTransaction: (clientId: string, modelName: string, modelId: string, original: Record<string, unknown>) => Transaction;
|
|
19
|
+
/**
|
|
20
|
+
* Creates an ARCHIVE transaction for soft-deleting a model instance
|
|
21
|
+
*/
|
|
22
|
+
export declare const createArchiveTransaction: (clientId: string, modelName: string, modelId: string, options?: ArchiveTransactionOptions) => Transaction;
|
|
23
|
+
/**
|
|
24
|
+
* Creates an UNARCHIVE transaction for restoring a soft-deleted model instance
|
|
25
|
+
*/
|
|
26
|
+
export declare const createUnarchiveTransaction: (clientId: string, modelName: string, modelId: string, options?: UnarchiveTransactionOptions) => Transaction;
|
|
27
|
+
/**
|
|
28
|
+
* Creates an undo transaction for a given transaction.
|
|
29
|
+
*/
|
|
30
|
+
export declare const createUndoTransaction: (tx: Transaction, clientId?: string) => Transaction | null;
|
|
31
|
+
//# sourceMappingURL=create.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../src/transaction/create.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,yBAAyB,EACzB,2BAA2B,EAC5B,MAAM,cAAc,CAAC;AAQtB,OAAO,KAAK,EAEV,WAAW,EACX,gBAAgB,EACjB,MAAM,YAAY,CAAC;AAyBpB;;GAEG;AACH,eAAO,MAAM,sBAAsB,GACjC,cAAc,WAAW,EAAE,KAC1B,gBAID,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,uBAAuB,GAClC,UAAU,MAAM,EAChB,WAAW,MAAM,EACjB,SAAS,MAAM,EACf,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC5B,WAOC,CAAC;AAEL;;GAEG;AACH,eAAO,MAAM,uBAAuB,GAClC,UAAU,MAAM,EAChB,WAAW,MAAM,EACjB,SAAS,MAAM,EACf,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,UAAU,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAChC,WAQC,CAAC;AAEL;;GAEG;AACH,eAAO,MAAM,uBAAuB,GAClC,UAAU,MAAM,EAChB,WAAW,MAAM,EACjB,SAAS,MAAM,EACf,UAAU,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAChC,WAQC,CAAC;AAEL;;GAEG;AACH,eAAO,MAAM,wBAAwB,GACnC,UAAU,MAAM,EAChB,WAAW,MAAM,EACjB,SAAS,MAAM,EACf,UAAS,yBAA8B,KACtC,WAQC,CAAC;AAEL;;GAEG;AACH,eAAO,MAAM,0BAA0B,GACrC,UAAU,MAAM,EAChB,WAAW,MAAM,EACjB,SAAS,MAAM,EACf,UAAS,2BAAgC,KACxC,WAQC,CAAC;AAEL;;GAEG;AACH,eAAO,MAAM,qBAAqB,GAChC,IAAI,WAAW,EACf,WAAU,MAAoB,KAC7B,WAAW,GAAG,IAgDhB,CAAC"}
|