@zukhruf/snapshot-differential 0.0.0-stage → 0.3.25
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/LICENSE +21 -0
- package/README.md +195 -2
- package/dist/closed-world.d.ts +5 -0
- package/dist/closed-world.d.ts.map +1 -0
- package/dist/closed-world.js +8 -0
- package/dist/closed-world.js.map +1 -0
- package/dist/coverage.d.ts +7 -0
- package/dist/coverage.d.ts.map +1 -0
- package/dist/coverage.js +15 -0
- package/dist/coverage.js.map +1 -0
- package/dist/deletion-policy.d.ts +10 -0
- package/dist/deletion-policy.d.ts.map +1 -0
- package/dist/deletion-policy.js +4 -0
- package/dist/deletion-policy.js.map +1 -0
- package/dist/fingerprint.d.ts +3 -0
- package/dist/fingerprint.d.ts.map +1 -0
- package/dist/fingerprint.js +27 -0
- package/dist/fingerprint.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/retention.d.ts +7 -0
- package/dist/retention.d.ts.map +1 -0
- package/dist/retention.js +20 -0
- package/dist/retention.js.map +1 -0
- package/dist/snapshot-differential.d.ts +40 -0
- package/dist/snapshot-differential.d.ts.map +1 -0
- package/dist/snapshot-differential.js +184 -0
- package/dist/snapshot-differential.js.map +1 -0
- package/dist/snapshot-state.d.ts +26 -0
- package/dist/snapshot-state.d.ts.map +1 -0
- package/dist/snapshot-state.js +80 -0
- package/dist/snapshot-state.js.map +1 -0
- package/dist/timestamp.d.ts +2 -0
- package/dist/timestamp.d.ts.map +1 -0
- package/dist/timestamp.js +8 -0
- package/dist/timestamp.js.map +1 -0
- package/package.json +42 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ezzabuzaid
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,196 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @zukhruf/snapshot-differential
|
|
2
2
|
|
|
3
|
-
This
|
|
3
|
+
An upstream with no change log, such as the SQLite store of a desktop app, can give only a list of its records. To load only what changed, you compare each new list with the last one. This package does that comparison: it is a snapshot differential (Labio and Garcia-Molina, "Efficient Snapshot Differential Algorithms for Data Warehousing", VLDB 1996), also known as change data capture by diff compare. A record that is not in the new list is not always deleted. A cache holds only part of its records, and an upstream with a time to live drops old records. Thus a deletion policy judges each such key.
|
|
4
|
+
|
|
5
|
+
The words in these documents have one meaning each. See the glossary in [CONTEXT.md](./CONTEXT.md).
|
|
6
|
+
|
|
7
|
+
## A diff yields the changes, then the snapshot
|
|
8
|
+
|
|
9
|
+
Make one `SnapshotDifferential` for each scan. Give it the primary key fields, a function that makes the key of a record, and a deletion policy. Then give `diff` the scan and the snapshot that the last committed scan saved:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import {
|
|
13
|
+
ClosedWorld,
|
|
14
|
+
SnapshotDifferential,
|
|
15
|
+
} from '@zukhruf/snapshot-differential';
|
|
16
|
+
|
|
17
|
+
type Contact = { id: string; name: string };
|
|
18
|
+
|
|
19
|
+
const differential = new SnapshotDifferential<Contact>({
|
|
20
|
+
name: 'contacts',
|
|
21
|
+
primaryKey: ['id'],
|
|
22
|
+
key: (contact) => JSON.stringify([contact.id]),
|
|
23
|
+
policy: new ClosedWorld(),
|
|
24
|
+
});
|
|
25
|
+
for await (const change of differential.diff(contacts, saved)) {
|
|
26
|
+
if (change.type === 'upsert') await upsert(change.record);
|
|
27
|
+
else if (change.type === 'delete') await remove(change.key);
|
|
28
|
+
else await save(change.state);
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- Give `null` as the snapshot for the first scan.
|
|
33
|
+
- The diff yields an upsert for each new or changed record, and nothing for a record that did not change.
|
|
34
|
+
- The diff yields the new snapshot last. Save it only after you applied the changes before it.
|
|
35
|
+
- Equal scans give byte-identical snapshots, in any order of records.
|
|
36
|
+
- A failed read must throw. An empty scan makes every key a vanished key.
|
|
37
|
+
|
|
38
|
+
## A deletion policy judges each vanished key
|
|
39
|
+
|
|
40
|
+
A vanished key is a key that the snapshot has and the scan does not list. The diff asks the policy about each one, and the policy gives one verdict:
|
|
41
|
+
|
|
42
|
+
| Verdict | Meaning | The diff |
|
|
43
|
+
| --------- | -------------------------------------------------------------------------- | ---------------------------------------------- |
|
|
44
|
+
| `deleted` | The upstream deleted the record (the closed-world assumption). | Yields a delete. The key leaves the snapshot. |
|
|
45
|
+
| `unknown` | The scan cannot tell (the open-world assumption). | Yields nothing. The key stays in the snapshot. |
|
|
46
|
+
| `expired` | The upstream dropped the record for good, with no deletion, as a TTL does. | Yields nothing. The key leaves the snapshot. |
|
|
47
|
+
|
|
48
|
+
The package has three policies:
|
|
49
|
+
|
|
50
|
+
- `ClosedWorld`: the scan lists every record, so each vanished key was deleted.
|
|
51
|
+
- `Retention(horizon)`: the upstream keeps records from the horizon on. A key whose expiry is before the horizon expired. Each other key was deleted. Give the differential the `expiresBy` field, so that the snapshot keeps the expiry of each key.
|
|
52
|
+
- `Coverage(covers)`: the upstream holds only part of its records, as a cache does. A vanished key that `covers` accepts was deleted. Each other key is unknown.
|
|
53
|
+
|
|
54
|
+
To judge keys in a different way, write a subclass of `DeletionPolicy`:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import {
|
|
58
|
+
DeletionPolicy,
|
|
59
|
+
type VanishedRecord,
|
|
60
|
+
type Verdict,
|
|
61
|
+
} from '@zukhruf/snapshot-differential';
|
|
62
|
+
|
|
63
|
+
// The app keeps a channel's messages only while the channel is open.
|
|
64
|
+
class OpenChannels extends DeletionPolicy {
|
|
65
|
+
readonly open: ReadonlySet<string>;
|
|
66
|
+
|
|
67
|
+
constructor(open: ReadonlySet<string>) {
|
|
68
|
+
super();
|
|
69
|
+
this.open = open;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
override vanished({ key }: VanishedRecord): Verdict {
|
|
73
|
+
return this.open.has(String(key.channel)) ? 'deleted' : 'unknown';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## An unknown key stays until a scan can tell
|
|
79
|
+
|
|
80
|
+
The policy judges an unknown key again at each scan:
|
|
81
|
+
|
|
82
|
+
- When a later scan covers the key and does not list it, the verdict is `deleted`, and the diff yields a delete.
|
|
83
|
+
- When a later scan lists the record again with the same fingerprint, the diff yields no upsert.
|
|
84
|
+
|
|
85
|
+
For example, a chat app drops a thread from its cache. Then a reply in that thread is deleted. Then the app loads the thread again. The reply is not in the thread, and the scan covers it, so the diff deletes it.
|
|
86
|
+
|
|
87
|
+
The snapshot keeps each unknown key. Thus the snapshot grows with the records that the upstream holds no more and that no scan can judge.
|
|
88
|
+
|
|
89
|
+
## A group with the same fingerprint is not read
|
|
90
|
+
|
|
91
|
+
When a scan comes from inputs that you can fingerprint cheaply, such as files by their size and time, give `diffGroups` one group for each input:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
for await (const change of differential.diffGroups(
|
|
95
|
+
files.map((file) => ({
|
|
96
|
+
key: file.path,
|
|
97
|
+
fingerprint: `${file.size}:${file.mtimeMs}`,
|
|
98
|
+
records: () => parse(file.path),
|
|
99
|
+
})),
|
|
100
|
+
saved,
|
|
101
|
+
)) {
|
|
102
|
+
// …
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
- A group with the same fingerprint as in the snapshot keeps its records. The diff does not call its `records`.
|
|
107
|
+
- A group with a `null` fingerprint is always read.
|
|
108
|
+
- A record that moves to a different group, with the same fingerprint, gets no upsert.
|
|
109
|
+
- The policy judges the vanished keys of the complete scan, also the keys of groups that are not in the scan.
|
|
110
|
+
|
|
111
|
+
## A lookback window keeps only what the next scan reads again
|
|
112
|
+
|
|
113
|
+
Some scans read a window, and the next scan reads only part of it again, such as the days that an upstream can still restate. Give `diff` a `rereads` function. Only the records that it accepts stay in the snapshot. A record outside the window leaves the snapshot, and the diff never deletes it.
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
differential.diff(days, saved, {
|
|
117
|
+
rereads: (day) => day.date >= settled,
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## A scan in parts deletes only at its last part
|
|
122
|
+
|
|
123
|
+
Some upstreams cannot list all their records in one call: an API that pages slowly and stops a client that reads too much. Give `diff` or `diffGroups` the option `continues: true` for each call that lists one part of the scan, and leave it out for the call that lists the last part:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
for await (const page of profile.pages(after)) {
|
|
127
|
+
for await (const change of differential.diff(page.posts, saved, {
|
|
128
|
+
continues: page.hasNext,
|
|
129
|
+
})) {
|
|
130
|
+
// …
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
- A part yields an upsert for each new or changed record that it lists. It does not wait for the last part.
|
|
136
|
+
- A part that continues the scan judges no vanished key. Its snapshot keeps every key, and the keys that the open scan listed so far.
|
|
137
|
+
- The last part judges each saved key that no part of the scan listed.
|
|
138
|
+
- In `diffGroups`, a record that leaves a group that a part reads again waits in the snapshot for the last part's judgment.
|
|
139
|
+
- Two parts can list the same key. One part cannot list a key twice.
|
|
140
|
+
- The parts can be in different runs. Save the snapshot after each part, and give it to the next part.
|
|
141
|
+
- The next scan starts with no listed keys.
|
|
142
|
+
|
|
143
|
+
## When something fails
|
|
144
|
+
|
|
145
|
+
Each failure is a `TypeError`, and the diff yields no snapshot:
|
|
146
|
+
|
|
147
|
+
- One call lists one key twice: `Snapshot <name> returned <key> twice in one scan`. In a scan in parts, this applies to each part.
|
|
148
|
+
- `diffGroups` gets one group key twice: `Snapshot <name> returned group <key> twice in one scan`.
|
|
149
|
+
- A record has a value that JSON cannot hold without a change, such as `undefined` or a `Date`.
|
|
150
|
+
- A record has no timestamp in its `expiresBy` field, in the format of `Date#toISOString`.
|
|
151
|
+
- The saved state is not a snapshot that this package wrote.
|
|
152
|
+
- `Retention` gets a horizon that is not a timestamp.
|
|
153
|
+
|
|
154
|
+
## The same concepts in other systems
|
|
155
|
+
|
|
156
|
+
| This package | Other systems |
|
|
157
|
+
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
158
|
+
| Snapshot differential | Labio and Garcia-Molina, VLDB 1996; change data capture by diff compare in Kimball and Caserta, _The Data Warehouse ETL Toolkit_ |
|
|
159
|
+
| `DeletionPolicy` | The Strategy pattern of Gamma et al., _Design Patterns_, which also names it Policy |
|
|
160
|
+
| `deleted`, `ClosedWorld` | The closed-world assumption of Reiter, "On Closed World Data Bases" (1978) |
|
|
161
|
+
| `unknown`, `Coverage` | The open-world assumption |
|
|
162
|
+
| `expired`, `Retention` | Cassandra's TTL; Kafka's `retention.ms` |
|
|
163
|
+
| `rereads` | The lookback of dbt incremental models; the re-sync window of Fivetran |
|
|
164
|
+
| `continues` | The incremental marking of a mark-and-sweep garbage collector: each part marks, and the last part sweeps |
|
|
165
|
+
| Groups and fingerprints | The up-to-date checks of Make and Bazel |
|
|
166
|
+
|
|
167
|
+
Airbyte has no equivalent. Outside change data capture, it deletes only when it replaces a complete stream, and its state decides what to read again, never what to delete.
|
|
168
|
+
|
|
169
|
+
## Documentation
|
|
170
|
+
|
|
171
|
+
- [ADR 0001: A vanished key is judged by a deletion policy](./docs/adr/0001-a-vanished-key-is-judged-by-a-deletion-policy.md)
|
|
172
|
+
- [ADR 0002: A scan can be listed in parts](./docs/adr/0002-a-scan-can-be-listed-in-parts.md)
|
|
173
|
+
- [ADR 0003: The differential is a zukhruf package](./docs/adr/0003-the-differential-is-a-zukhruf-package.md)
|
|
174
|
+
|
|
175
|
+
## Development
|
|
176
|
+
|
|
177
|
+
Run the commands from the workspace root.
|
|
178
|
+
|
|
179
|
+
```sh
|
|
180
|
+
npx nx run snapshot-differential:test # builds, then runs the tests in dist/
|
|
181
|
+
npx nx run snapshot-differential:typecheck # formats, lints, then type checks
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
src/
|
|
186
|
+
snapshot-differential.ts SnapshotDifferential: diff, diffGroups, and the judgment of vanished keys
|
|
187
|
+
snapshot-state.ts the saved snapshot and its guards
|
|
188
|
+
deletion-policy.ts DeletionPolicy, VanishedRecord, Verdict
|
|
189
|
+
closed-world.ts ClosedWorld
|
|
190
|
+
retention.ts Retention
|
|
191
|
+
coverage.ts Coverage
|
|
192
|
+
fingerprint.ts fingerprints over canonical JSON
|
|
193
|
+
timestamp.ts the timestamp check
|
|
194
|
+
index.ts the public exports
|
|
195
|
+
snapshot-differential.test.ts the tests
|
|
196
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"closed-world.d.ts","sourceRoot":"","sources":["../src/closed-world.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,KAAK,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAGpE,qBAAa,WAAY,SAAQ,cAAc;IACpC,QAAQ,IAAI,OAAO;CAG7B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"closed-world.js","sourceRoot":"","sources":["../src/closed-world.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAgB,MAAM,sBAAsB,CAAC;AAEpE,sEAAsE;AACtE,MAAM,OAAO,WAAY,SAAQ,cAAc;IACpC,QAAQ;QACf,OAAO,SAAS,CAAC;IACnB,CAAC;CACF"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { DeletionPolicy, type VanishedRecord, type Verdict } from './deletion-policy.ts';
|
|
2
|
+
export declare class Coverage extends DeletionPolicy {
|
|
3
|
+
#private;
|
|
4
|
+
constructor(covers: (record: VanishedRecord) => boolean);
|
|
5
|
+
vanished(record: VanishedRecord): Verdict;
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=coverage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coverage.d.ts","sourceRoot":"","sources":["../src/coverage.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,KAAK,cAAc,EACnB,KAAK,OAAO,EACb,MAAM,sBAAsB,CAAC;AAK9B,qBAAa,QAAS,SAAQ,cAAc;;gBAG9B,MAAM,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,OAAO;IAK9C,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO;CAGnD"}
|
package/dist/coverage.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { DeletionPolicy, } from "./deletion-policy.js";
|
|
2
|
+
// An upstream that holds only part of its records and says which part, as a
|
|
3
|
+
// cache does: a vanished key the scan covers was deleted; one it does not
|
|
4
|
+
// cover is unknown until a later scan covers it or lists it again.
|
|
5
|
+
export class Coverage extends DeletionPolicy {
|
|
6
|
+
#covers;
|
|
7
|
+
constructor(covers) {
|
|
8
|
+
super();
|
|
9
|
+
this.#covers = covers;
|
|
10
|
+
}
|
|
11
|
+
vanished(record) {
|
|
12
|
+
return this.#covers(record) ? 'deleted' : 'unknown';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=coverage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coverage.js","sourceRoot":"","sources":["../src/coverage.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,GAGf,MAAM,sBAAsB,CAAC;AAE9B,4EAA4E;AAC5E,0EAA0E;AAC1E,mEAAmE;AACnE,MAAM,OAAO,QAAS,SAAQ,cAAc;IACjC,OAAO,CAAsC;IAEtD,YAAY,MAA2C;QACrD,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;IACxB,CAAC;IAEQ,QAAQ,CAAC,MAAsB;QACtC,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;IACtD,CAAC;CACF"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type KeyValue = string | number | boolean;
|
|
2
|
+
export type VanishedRecord = {
|
|
3
|
+
readonly key: Readonly<Record<string, KeyValue>>;
|
|
4
|
+
readonly expiresBy?: string;
|
|
5
|
+
};
|
|
6
|
+
export type Verdict = 'deleted' | 'unknown' | 'expired';
|
|
7
|
+
export declare abstract class DeletionPolicy {
|
|
8
|
+
abstract vanished(record: VanishedRecord): Verdict;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=deletion-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deletion-policy.d.ts","sourceRoot":"","sources":["../src/deletion-policy.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAGjD,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;IAEjD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC;AAOF,MAAM,MAAM,OAAO,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;AAGxD,8BAAsB,cAAc;IAClC,QAAQ,CAAC,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO;CACnD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deletion-policy.js","sourceRoot":"","sources":["../src/deletion-policy.ts"],"names":[],"mappings":"AAgBA,qEAAqE;AACrE,MAAM,OAAgB,cAAc;CAEnC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fingerprint.d.ts","sourceRoot":"","sources":["../src/fingerprint.ts"],"names":[],"mappings":"AAIA,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,MAAM,CAOR;AAaD,wBAAgB,aAAa,CAC3B,KAAK,EAAE,OAAO,GACb,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAOlC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
3
|
+
// A record's SHA-256 over its canonical JSON, so equal records hash equally.
|
|
4
|
+
export function fingerprintOf(name, record) {
|
|
5
|
+
const serialized = JSON.parse(JSON.stringify(record));
|
|
6
|
+
if (!isDeepStrictEqual(serialized, record))
|
|
7
|
+
throw new TypeError(`Snapshot ${name} records must be losslessly JSON serializable to diff snapshots`);
|
|
8
|
+
return createHash('sha256').update(canonical(serialized)).digest('base64url');
|
|
9
|
+
}
|
|
10
|
+
// JSON with object keys sorted at every depth.
|
|
11
|
+
function canonical(value) {
|
|
12
|
+
if (Array.isArray(value))
|
|
13
|
+
return `[${value.map(canonical).join(',')}]`;
|
|
14
|
+
if (isPlainObject(value))
|
|
15
|
+
return `{${Object.keys(value)
|
|
16
|
+
.sort()
|
|
17
|
+
.map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`)
|
|
18
|
+
.join(',')}}`;
|
|
19
|
+
return JSON.stringify(value);
|
|
20
|
+
}
|
|
21
|
+
export function isPlainObject(value) {
|
|
22
|
+
return (value !== null &&
|
|
23
|
+
typeof value === 'object' &&
|
|
24
|
+
!Array.isArray(value) &&
|
|
25
|
+
Object.getPrototypeOf(value) === Object.prototype);
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=fingerprint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fingerprint.js","sourceRoot":"","sources":["../src/fingerprint.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAE9C,6EAA6E;AAC7E,MAAM,UAAU,aAAa,CAC3B,IAAY,EACZ,MAA+B;IAE/B,MAAM,UAAU,GAAY,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/D,IAAI,CAAC,iBAAiB,CAAC,UAAU,EAAE,MAAM,CAAC;QACxC,MAAM,IAAI,SAAS,CACjB,YAAY,IAAI,iEAAiE,CAClF,CAAC;IACJ,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;AAChF,CAAC;AAED,+CAA+C;AAC/C,SAAS,SAAS,CAAC,KAAc;IAC/B,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;IACvE,IAAI,aAAa,CAAC,KAAK,CAAC;QACtB,OAAO,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC;aAC1B,IAAI,EAAE;aACN,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;aAC/D,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;IAClB,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;AAC/B,CAAC;AAED,MAAM,UAAU,aAAa,CAC3B,KAAc;IAEd,OAAO,CACL,KAAK,KAAK,IAAI;QACd,OAAO,KAAK,KAAK,QAAQ;QACzB,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QACrB,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,KAAK,MAAM,CAAC,SAAS,CAClD,CAAC;AACJ,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { ClosedWorld } from './closed-world.ts';
|
|
2
|
+
export { Coverage } from './coverage.ts';
|
|
3
|
+
export { DeletionPolicy, type KeyValue, type VanishedRecord, type Verdict, } from './deletion-policy.ts';
|
|
4
|
+
export { Retention } from './retention.ts';
|
|
5
|
+
export { type SnapshotChange, SnapshotDifferential, type SnapshotDifferentialOptions, type SnapshotGroup, } from './snapshot-differential.ts';
|
|
6
|
+
export type { GroupedSnapshotState, SnapshotState } from './snapshot-state.ts';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EACL,cAAc,EACd,KAAK,QAAQ,EACb,KAAK,cAAc,EACnB,KAAK,OAAO,GACb,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EACL,KAAK,cAAc,EACnB,oBAAoB,EACpB,KAAK,2BAA2B,EAChC,KAAK,aAAa,GACnB,MAAM,4BAA4B,CAAC;AACpC,YAAY,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { ClosedWorld } from "./closed-world.js";
|
|
2
|
+
export { Coverage } from "./coverage.js";
|
|
3
|
+
export { DeletionPolicy, } from "./deletion-policy.js";
|
|
4
|
+
export { Retention } from "./retention.js";
|
|
5
|
+
export { SnapshotDifferential, } from "./snapshot-differential.js";
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EACL,cAAc,GAIf,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAEL,oBAAoB,GAGrB,MAAM,4BAA4B,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { DeletionPolicy, type VanishedRecord, type Verdict } from './deletion-policy.ts';
|
|
2
|
+
export declare class Retention extends DeletionPolicy {
|
|
3
|
+
readonly horizon: string;
|
|
4
|
+
constructor(horizon: string);
|
|
5
|
+
vanished({ expiresBy }: VanishedRecord): Verdict;
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=retention.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retention.d.ts","sourceRoot":"","sources":["../src/retention.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,KAAK,cAAc,EACnB,KAAK,OAAO,EACb,MAAM,sBAAsB,CAAC;AAM9B,qBAAa,SAAU,SAAQ,cAAc;IAC3C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;gBAEb,OAAO,EAAE,MAAM;IAOlB,QAAQ,CAAC,EAAE,SAAS,EAAE,EAAE,cAAc,GAAG,OAAO;CAK1D"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { DeletionPolicy, } from "./deletion-policy.js";
|
|
2
|
+
import { isTimestamp } from "./timestamp.js";
|
|
3
|
+
// An upstream that keeps records from a horizon on and drops older ones
|
|
4
|
+
// without a deletion, as a time to live does: a key that vanished with an
|
|
5
|
+
// expiry before the horizon expired, any other was deleted.
|
|
6
|
+
export class Retention extends DeletionPolicy {
|
|
7
|
+
horizon;
|
|
8
|
+
constructor(horizon) {
|
|
9
|
+
super();
|
|
10
|
+
if (!isTimestamp(horizon))
|
|
11
|
+
throw new TypeError(`Retention horizon ${horizon} is not a timestamp`);
|
|
12
|
+
this.horizon = horizon;
|
|
13
|
+
}
|
|
14
|
+
vanished({ expiresBy }) {
|
|
15
|
+
return expiresBy === undefined || expiresBy >= this.horizon
|
|
16
|
+
? 'deleted'
|
|
17
|
+
: 'expired';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=retention.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retention.js","sourceRoot":"","sources":["../src/retention.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,GAGf,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE7C,wEAAwE;AACxE,0EAA0E;AAC1E,4DAA4D;AAC5D,MAAM,OAAO,SAAU,SAAQ,cAAc;IAClC,OAAO,CAAS;IAEzB,YAAY,OAAe;QACzB,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC;YACvB,MAAM,IAAI,SAAS,CAAC,qBAAqB,OAAO,qBAAqB,CAAC,CAAC;QACzE,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;IAEQ,QAAQ,CAAC,EAAE,SAAS,EAAkB;QAC7C,OAAO,SAAS,KAAK,SAAS,IAAI,SAAS,IAAI,IAAI,CAAC,OAAO;YACzD,CAAC,CAAC,SAAS;YACX,CAAC,CAAC,SAAS,CAAC;IAChB,CAAC;CACF"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { DeletionPolicy, KeyValue } from './deletion-policy.ts';
|
|
2
|
+
import { type GroupedSnapshotState, type SnapshotState } from './snapshot-state.ts';
|
|
3
|
+
export type SnapshotDifferentialOptions<Data> = {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
readonly primaryKey: readonly string[];
|
|
6
|
+
readonly key: (record: Data) => string;
|
|
7
|
+
readonly expiresBy?: string | undefined;
|
|
8
|
+
readonly policy: DeletionPolicy;
|
|
9
|
+
};
|
|
10
|
+
export type SnapshotGroup<Data> = {
|
|
11
|
+
readonly key: string;
|
|
12
|
+
readonly fingerprint: string | null;
|
|
13
|
+
records(): AsyncIterable<Data> | Iterable<Data>;
|
|
14
|
+
};
|
|
15
|
+
export type SnapshotChange<Data, State> = {
|
|
16
|
+
readonly type: 'upsert';
|
|
17
|
+
readonly record: Data;
|
|
18
|
+
} | {
|
|
19
|
+
readonly type: 'delete';
|
|
20
|
+
readonly key: Readonly<Record<string, KeyValue>>;
|
|
21
|
+
} | {
|
|
22
|
+
readonly type: 'state';
|
|
23
|
+
readonly state: State;
|
|
24
|
+
};
|
|
25
|
+
export declare class SnapshotDifferential<Data extends Record<string, unknown>> {
|
|
26
|
+
#private;
|
|
27
|
+
readonly name: string;
|
|
28
|
+
readonly primaryKey: readonly string[];
|
|
29
|
+
readonly expiresBy: string | undefined;
|
|
30
|
+
readonly policy: DeletionPolicy;
|
|
31
|
+
constructor({ name, primaryKey, key, expiresBy, policy, }: SnapshotDifferentialOptions<Data>);
|
|
32
|
+
diff(records: AsyncIterable<Data> | Iterable<Data>, state: unknown, { rereads, continues, }?: {
|
|
33
|
+
readonly rereads?: (record: Data) => boolean;
|
|
34
|
+
readonly continues?: boolean;
|
|
35
|
+
}): AsyncGenerator<SnapshotChange<Data, SnapshotState>>;
|
|
36
|
+
diffGroups(groups: AsyncIterable<SnapshotGroup<Data>> | Iterable<SnapshotGroup<Data>>, state: unknown, { continues }?: {
|
|
37
|
+
readonly continues?: boolean;
|
|
38
|
+
}): AsyncGenerator<SnapshotChange<Data, GroupedSnapshotState>>;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=snapshot-differential.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot-differential.d.ts","sourceRoot":"","sources":["../src/snapshot-differential.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,QAAQ,EAET,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACL,KAAK,oBAAoB,EAGzB,KAAK,aAAa,EAKnB,MAAM,qBAAqB,CAAC;AAG7B,MAAM,MAAM,2BAA2B,CAAC,IAAI,IAAI;IAE9C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IAEvC,QAAQ,CAAC,GAAG,EAAE,CAAC,MAAM,EAAE,IAAI,KAAK,MAAM,CAAC;IAGvC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;CACjC,CAAC;AAIF,MAAM,MAAM,aAAa,CAAC,IAAI,IAAI;IAChC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAErB,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,OAAO,IAAI,aAAa,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;CACjD,CAAC;AAEF,MAAM,MAAM,cAAc,CAAC,IAAI,EAAE,KAAK,IAClC;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAA;CAAE,GAClD;IACE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;CAClD,GACD;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC;AAOtD,qBAAa,oBAAoB,CAAC,IAAI,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;IACpE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;gBAGpB,EACV,IAAI,EACJ,UAAU,EACV,GAAG,EACH,SAAS,EACT,MAAM,GACP,EAAE,2BAA2B,CAAC,IAAI,CAAC;IAe7B,IAAI,CACT,OAAO,EAAE,aAAa,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,EAC7C,KAAK,EAAE,OAAO,EACd,EACE,OAAO,EACP,SAAiB,GAClB,GAAE;QACD,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,IAAI,KAAK,OAAO,CAAC;QAC7C,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;KACzB,GACL,cAAc,CAAC,cAAc,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC;IAmC/C,UAAU,CACf,MAAM,EAAE,aAAa,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,GAAG,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,EAC1E,KAAK,EAAE,OAAO,EACd,EAAE,SAAiB,EAAE,GAAE;QAAE,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAA;KAAO,GAC3D,cAAc,CAAC,cAAc,CAAC,IAAI,EAAE,oBAAoB,CAAC,CAAC;CAiJ9D"}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { fingerprintOf } from "./fingerprint.js";
|
|
2
|
+
import { fingerprintIn, readGroups, readSnapshot, sorted, } from "./snapshot-state.js";
|
|
3
|
+
import { isTimestamp } from "./timestamp.js";
|
|
4
|
+
// A snapshot differential (Labio and Garcia-Molina, VLDB 1996): change data
|
|
5
|
+
// capture for an upstream with no change log. It compares one complete scan
|
|
6
|
+
// with the previous snapshot and yields each new or changed record, a delete
|
|
7
|
+
// for each key the policy judges deleted, and the new snapshot last. An empty
|
|
8
|
+
// scan judges every key, so a failed read must throw, never yield nothing.
|
|
9
|
+
export class SnapshotDifferential {
|
|
10
|
+
name;
|
|
11
|
+
primaryKey;
|
|
12
|
+
expiresBy;
|
|
13
|
+
policy;
|
|
14
|
+
#key;
|
|
15
|
+
constructor({ name, primaryKey, key, expiresBy, policy, }) {
|
|
16
|
+
this.name = name;
|
|
17
|
+
this.primaryKey = primaryKey;
|
|
18
|
+
this.#key = key;
|
|
19
|
+
this.expiresBy = expiresBy;
|
|
20
|
+
this.policy = policy;
|
|
21
|
+
}
|
|
22
|
+
// rereads names the lookback window: a scan the next one repeats only in
|
|
23
|
+
// part, such as days an upstream may still restate. Only records the next
|
|
24
|
+
// scan reads again stay in the snapshot; one left out is never deleted.
|
|
25
|
+
// continues makes this call one part of a scan that later calls go on
|
|
26
|
+
// with, for an upstream that cannot list everything at once: the part
|
|
27
|
+
// judges no vanished key and saves the keys it listed, and the part that
|
|
28
|
+
// ends the scan judges every saved key that no part listed.
|
|
29
|
+
async *diff(records, state, { rereads, continues = false, } = {}) {
|
|
30
|
+
const { snapshot: previous, listed } = readSnapshot(this.name, state);
|
|
31
|
+
const seen = new Set();
|
|
32
|
+
const current = new Map();
|
|
33
|
+
for await (const record of records) {
|
|
34
|
+
const key = this.#claim(seen, record);
|
|
35
|
+
const fingerprint = fingerprintOf(this.name, record);
|
|
36
|
+
if (rereads?.(record) ?? true)
|
|
37
|
+
current.set(key, this.#entry(record, fingerprint));
|
|
38
|
+
if (fingerprintIn(previous.get(key)) !== fingerprint)
|
|
39
|
+
yield { type: 'upsert', record };
|
|
40
|
+
}
|
|
41
|
+
const listedSoFar = new Set([...listed, ...seen]);
|
|
42
|
+
for (const [key, entry] of previous)
|
|
43
|
+
if (!seen.has(key) && (continues || listed.has(key)))
|
|
44
|
+
current.set(key, entry);
|
|
45
|
+
if (continues) {
|
|
46
|
+
yield {
|
|
47
|
+
type: 'state',
|
|
48
|
+
state: {
|
|
49
|
+
snapshot: sorted(current),
|
|
50
|
+
listed: [...listedSoFar].sort(),
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
yield* this.#judge(previous, listedSoFar, current);
|
|
56
|
+
yield { type: 'state', state: { snapshot: sorted(current) } };
|
|
57
|
+
}
|
|
58
|
+
// diff for a scan whose records come from inputs the source fingerprints
|
|
59
|
+
// cheaply. A group whose fingerprint matches the last committed scan keeps
|
|
60
|
+
// its records without reading them; every other group is read and diffed
|
|
61
|
+
// record by record. Vanished keys are judged over the complete scan.
|
|
62
|
+
// continues makes this call one part of a scan in parts, as for diff.
|
|
63
|
+
async *diffGroups(groups, state, { continues = false } = {}) {
|
|
64
|
+
const previous = readGroups(this.name, state);
|
|
65
|
+
// Every saved entry by key, so a record that moves between groups or
|
|
66
|
+
// comes back from unknown is compared with its previous fingerprint.
|
|
67
|
+
const saved = new Map([
|
|
68
|
+
...[...previous.groups.values()].flatMap(({ snapshot }) => Object.entries(snapshot)),
|
|
69
|
+
...previous.unknown,
|
|
70
|
+
]);
|
|
71
|
+
const seen = new Set();
|
|
72
|
+
const current = new Map();
|
|
73
|
+
for await (const group of groups) {
|
|
74
|
+
if (current.has(group.key))
|
|
75
|
+
throw new TypeError(`Snapshot ${this.name} returned group ${group.key} twice in one scan`);
|
|
76
|
+
const before = previous.groups.get(group.key);
|
|
77
|
+
if (group.fingerprint !== null &&
|
|
78
|
+
before?.fingerprint === group.fingerprint) {
|
|
79
|
+
for (const key of Object.keys(before.snapshot))
|
|
80
|
+
this.#claimKey(seen, key);
|
|
81
|
+
current.set(group.key, before);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const snapshot = new Map();
|
|
85
|
+
for await (const record of group.records()) {
|
|
86
|
+
const key = this.#claim(seen, record);
|
|
87
|
+
const fingerprint = fingerprintOf(this.name, record);
|
|
88
|
+
snapshot.set(key, this.#entry(record, fingerprint));
|
|
89
|
+
if (fingerprintIn(saved.get(key)) !== fingerprint)
|
|
90
|
+
yield { type: 'upsert', record };
|
|
91
|
+
}
|
|
92
|
+
current.set(group.key, {
|
|
93
|
+
fingerprint: group.fingerprint,
|
|
94
|
+
snapshot: sorted(snapshot),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
const listedSoFar = new Set([...previous.listed, ...current.keys()]);
|
|
98
|
+
// A group this call did not list keeps what the scan saw of it while the
|
|
99
|
+
// scan goes on, or when an earlier part listed it; a record that moved
|
|
100
|
+
// into a group this call listed leaves it.
|
|
101
|
+
for (const [key, before] of previous.groups) {
|
|
102
|
+
if (current.has(key) || !(continues || previous.listed.has(key)))
|
|
103
|
+
continue;
|
|
104
|
+
const snapshot = Object.entries(before.snapshot).filter(([recordKey]) => !seen.has(recordKey));
|
|
105
|
+
for (const [recordKey] of snapshot)
|
|
106
|
+
seen.add(recordKey);
|
|
107
|
+
current.set(key, {
|
|
108
|
+
fingerprint: before.fingerprint,
|
|
109
|
+
snapshot: Object.fromEntries(snapshot),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
const unknown = new Map();
|
|
113
|
+
if (continues) {
|
|
114
|
+
// A key no group holds any more, such as one that vanished from a
|
|
115
|
+
// group this part read again, waits for the last part's judgment.
|
|
116
|
+
const grouped = new Set([...current.values()].flatMap(({ snapshot }) => Object.keys(snapshot)));
|
|
117
|
+
for (const [key, entry] of saved)
|
|
118
|
+
if (!seen.has(key) && !grouped.has(key))
|
|
119
|
+
unknown.set(key, entry);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
yield* this.#judge(saved, seen, unknown);
|
|
123
|
+
}
|
|
124
|
+
yield {
|
|
125
|
+
type: 'state',
|
|
126
|
+
state: {
|
|
127
|
+
groups: sorted(current),
|
|
128
|
+
...(unknown.size === 0 ? {} : { unknown: sorted(unknown) }),
|
|
129
|
+
...(continues ? { listed: [...listedSoFar].sort() } : {}),
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
// Asks the policy about every saved key the scan did not list. A deleted
|
|
134
|
+
// one yields a delete, an unknown one stays in kept, an expired one leaves.
|
|
135
|
+
*#judge(saved, seen, kept) {
|
|
136
|
+
for (const [key, entry] of saved) {
|
|
137
|
+
if (seen.has(key))
|
|
138
|
+
continue;
|
|
139
|
+
const record = typeof entry === 'string'
|
|
140
|
+
? { key: this.#keyObject(key) }
|
|
141
|
+
: { key: this.#keyObject(key), expiresBy: entry[1] };
|
|
142
|
+
const verdict = this.policy.vanished(record);
|
|
143
|
+
if (verdict === 'deleted')
|
|
144
|
+
yield { type: 'delete', key: record.key };
|
|
145
|
+
else if (verdict === 'unknown')
|
|
146
|
+
kept.set(key, entry);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
#claim(seen, record) {
|
|
150
|
+
const key = this.#key(record);
|
|
151
|
+
this.#claimKey(seen, key);
|
|
152
|
+
return key;
|
|
153
|
+
}
|
|
154
|
+
#claimKey(seen, key) {
|
|
155
|
+
if (seen.has(key))
|
|
156
|
+
throw new TypeError(`Snapshot ${this.name} returned ${this.#keyText(key)} twice in one scan`);
|
|
157
|
+
seen.add(key);
|
|
158
|
+
}
|
|
159
|
+
#entry(record, fingerprint) {
|
|
160
|
+
if (this.expiresBy === undefined)
|
|
161
|
+
return fingerprint;
|
|
162
|
+
const expiresBy = record[this.expiresBy];
|
|
163
|
+
if (!isTimestamp(expiresBy))
|
|
164
|
+
throw new TypeError(`Snapshot ${this.name} records must carry ${this.expiresBy} as a timestamp to expire`);
|
|
165
|
+
return [fingerprint, expiresBy];
|
|
166
|
+
}
|
|
167
|
+
// A key as its primary key fields, from the JSON of its values.
|
|
168
|
+
#keyObject(key) {
|
|
169
|
+
const values = JSON.parse(key);
|
|
170
|
+
return Object.fromEntries(this.primaryKey.map((field, index) => {
|
|
171
|
+
const value = values[index];
|
|
172
|
+
if (value === undefined)
|
|
173
|
+
throw new TypeError(`Snapshot key ${key} does not match ${this.name}'s primary key`);
|
|
174
|
+
return [field, value];
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
// A key as people read it: each field with its value, a string as it is.
|
|
178
|
+
#keyText(key) {
|
|
179
|
+
return Object.entries(this.#keyObject(key))
|
|
180
|
+
.map(([field, value]) => `${field} ${typeof value === 'string' ? value : JSON.stringify(value)}`)
|
|
181
|
+
.join(', ');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=snapshot-differential.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot-differential.js","sourceRoot":"","sources":["../src/snapshot-differential.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAKL,aAAa,EACb,UAAU,EACV,YAAY,EACZ,MAAM,GACP,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AA+B7C,4EAA4E;AAC5E,4EAA4E;AAC5E,6EAA6E;AAC7E,8EAA8E;AAC9E,2EAA2E;AAC3E,MAAM,OAAO,oBAAoB;IACtB,IAAI,CAAS;IACb,UAAU,CAAoB;IAC9B,SAAS,CAAqB;IAC9B,MAAM,CAAiB;IACvB,IAAI,CAA2B;IAExC,YAAY,EACV,IAAI,EACJ,UAAU,EACV,GAAG,EACH,SAAS,EACT,MAAM,GAC4B;QAClC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC;QAChB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;IAED,yEAAyE;IACzE,0EAA0E;IAC1E,wEAAwE;IACxE,sEAAsE;IACtE,sEAAsE;IACtE,yEAAyE;IACzE,4DAA4D;IAC5D,KAAK,CAAC,CAAC,IAAI,CACT,OAA6C,EAC7C,KAAc,EACd,EACE,OAAO,EACP,SAAS,GAAG,KAAK,MAIf,EAAE;QAEN,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QACtE,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;QAC/B,MAAM,OAAO,GAAG,IAAI,GAAG,EAAsB,CAAC;QAC9C,IAAI,KAAK,EAAE,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YACnC,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YACtC,MAAM,WAAW,GAAG,aAAa,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YACrD,IAAI,OAAO,EAAE,CAAC,MAAM,CAAC,IAAI,IAAI;gBAC3B,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC;YACrD,IAAI,aAAa,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,KAAK,WAAW;gBAClD,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;QACrC,CAAC;QACD,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC;QAClD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,QAAQ;YACjC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,IAAI,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;gBAClD,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAC5B,IAAI,SAAS,EAAE,CAAC;YACd,MAAM;gBACJ,IAAI,EAAE,OAAO;gBACb,KAAK,EAAE;oBACL,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC;oBACzB,MAAM,EAAE,CAAC,GAAG,WAAW,CAAC,CAAC,IAAI,EAAE;iBAChC;aACF,CAAC;YACF,OAAO;QACT,CAAC;QACD,KAAK,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC;QACnD,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC;IAChE,CAAC;IAED,yEAAyE;IACzE,2EAA2E;IAC3E,yEAAyE;IACzE,qEAAqE;IACrE,sEAAsE;IACtE,KAAK,CAAC,CAAC,UAAU,CACf,MAA0E,EAC1E,KAAc,EACd,EAAE,SAAS,GAAG,KAAK,KAAuC,EAAE;QAE5D,MAAM,QAAQ,GAAG,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAC9C,qEAAqE;QACrE,qEAAqE;QACrE,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC;YACpB,GAAG,CAAC,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CACxD,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,CACzB;YACD,GAAG,QAAQ,CAAC,OAAO;SACpB,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;QAC/B,MAAM,OAAO,GAAG,IAAI,GAAG,EAAsB,CAAC;QAC9C,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YACjC,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC;gBACxB,MAAM,IAAI,SAAS,CACjB,YAAY,IAAI,CAAC,IAAI,mBAAmB,KAAK,CAAC,GAAG,oBAAoB,CACtE,CAAC;YACJ,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YAC9C,IACE,KAAK,CAAC,WAAW,KAAK,IAAI;gBAC1B,MAAM,EAAE,WAAW,KAAK,KAAK,CAAC,WAAW,EACzC,CAAC;gBACD,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC;oBAC5C,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;gBAC5B,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;gBAC/B,SAAS;YACX,CAAC;YACD,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAsB,CAAC;YAC/C,IAAI,KAAK,EAAE,MAAM,MAAM,IAAI,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC;gBAC3C,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;gBACtC,MAAM,WAAW,GAAG,aAAa,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;gBACrD,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC;gBACpD,IAAI,aAAa,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,KAAK,WAAW;oBAC/C,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;YACrC,CAAC;YACD,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE;gBACrB,WAAW,EAAE,KAAK,CAAC,WAAW;gBAC9B,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC;aAC3B,CAAC,CAAC;QACL,CAAC;QACD,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QACrE,yEAAyE;QACzE,uEAAuE;QACvE,2CAA2C;QAC3C,KAAK,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;YAC5C,IAAI,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,SAAS,IAAI,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;gBAC9D,SAAS;YACX,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,CACrD,CAAC,CAAC,SAAS,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CACtC,CAAC;YACF,KAAK,MAAM,CAAC,SAAS,CAAC,IAAI,QAAQ;gBAAE,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YACxD,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE;gBACf,WAAW,EAAE,MAAM,CAAC,WAAW;gBAC/B,QAAQ,EAAE,MAAM,CAAC,WAAW,CAAC,QAAQ,CAAC;aACvC,CAAC,CAAC;QACL,CAAC;QACD,MAAM,OAAO,GAAG,IAAI,GAAG,EAAsB,CAAC;QAC9C,IAAI,SAAS,EAAE,CAAC;YACd,kEAAkE;YAClE,kEAAkE;YAClE,MAAM,OAAO,GAAG,IAAI,GAAG,CACrB,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CACvE,CAAC;YACF,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,KAAK;gBAC9B,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC;oBAAE,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACrE,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;QAC3C,CAAC;QACD,MAAM;YACJ,IAAI,EAAE,OAAO;YACb,KAAK,EAAE;gBACL,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC;gBACvB,GAAG,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC3D,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,GAAG,WAAW,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC1D;SACF,CAAC;IACJ,CAAC;IAED,yEAAyE;IACzE,4EAA4E;IAC5E,CAAC,MAAM,CACL,KAAsC,EACtC,IAAyB,EACzB,IAA6B;QAE7B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,KAAK,EAAE,CAAC;YACjC,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;gBAAE,SAAS;YAC5B,MAAM,MAAM,GACV,OAAO,KAAK,KAAK,QAAQ;gBACvB,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE;gBAC/B,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;YACzD,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;YAC7C,IAAI,OAAO,KAAK,SAAS;gBAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC;iBAChE,IAAI,OAAO,KAAK,SAAS;gBAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACvD,CAAC;IACH,CAAC;IAED,MAAM,CAAC,IAAiB,EAAE,MAAY;QACpC,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC9B,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QAC1B,OAAO,GAAG,CAAC;IACb,CAAC;IAED,SAAS,CAAC,IAAiB,EAAE,GAAW;QACtC,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;YACf,MAAM,IAAI,SAAS,CACjB,YAAY,IAAI,CAAC,IAAI,aAAa,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,oBAAoB,CACzE,CAAC;QACJ,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAChB,CAAC;IAED,MAAM,CAAC,MAAY,EAAE,WAAmB;QACtC,IAAI,IAAI,CAAC,SAAS,KAAK,SAAS;YAAE,OAAO,WAAW,CAAC;QACrD,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACzC,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC;YACzB,MAAM,IAAI,SAAS,CACjB,YAAY,IAAI,CAAC,IAAI,uBAAuB,IAAI,CAAC,SAAS,2BAA2B,CACtF,CAAC;QACJ,OAAO,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;IAClC,CAAC;IAED,gEAAgE;IAChE,UAAU,CAAC,GAAW;QACpB,MAAM,MAAM,GAAe,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC3C,OAAO,MAAM,CAAC,WAAW,CACvB,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;YACnC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;YAC5B,IAAI,KAAK,KAAK,SAAS;gBACrB,MAAM,IAAI,SAAS,CACjB,gBAAgB,GAAG,mBAAmB,IAAI,CAAC,IAAI,gBAAgB,CAChE,CAAC;YACJ,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QACxB,CAAC,CAAC,CACH,CAAC;IACJ,CAAC;IAED,yEAAyE;IACzE,QAAQ,CAAC,GAAW;QAClB,OAAO,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;aACxC,GAAG,CACF,CAAC,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,EAAE,CACjB,GAAG,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAC1E;aACA,IAAI,CAAC,IAAI,CAAC,CAAC;IAChB,CAAC;CACF"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type SavedEntry = string | readonly [fingerprint: string, expiresBy: string];
|
|
2
|
+
export type SnapshotState = {
|
|
3
|
+
readonly snapshot: Readonly<Record<string, SavedEntry>>;
|
|
4
|
+
readonly listed?: readonly string[];
|
|
5
|
+
};
|
|
6
|
+
export type SavedGroup = {
|
|
7
|
+
readonly fingerprint: string | null;
|
|
8
|
+
readonly snapshot: Readonly<Record<string, SavedEntry>>;
|
|
9
|
+
};
|
|
10
|
+
export type GroupedSnapshotState = {
|
|
11
|
+
readonly groups: Readonly<Record<string, SavedGroup>>;
|
|
12
|
+
readonly unknown?: Readonly<Record<string, SavedEntry>>;
|
|
13
|
+
readonly listed?: readonly string[];
|
|
14
|
+
};
|
|
15
|
+
export declare function readSnapshot(name: string, state: unknown): {
|
|
16
|
+
snapshot: Map<string, SavedEntry>;
|
|
17
|
+
listed: Set<string>;
|
|
18
|
+
};
|
|
19
|
+
export declare function readGroups(name: string, state: unknown): {
|
|
20
|
+
groups: Map<string, SavedGroup>;
|
|
21
|
+
unknown: Map<string, SavedEntry>;
|
|
22
|
+
listed: Set<string>;
|
|
23
|
+
};
|
|
24
|
+
export declare function sorted<Value>(values: ReadonlyMap<string, Value>): Record<string, Value>;
|
|
25
|
+
export declare function fingerprintIn(entry: SavedEntry | undefined): string | undefined;
|
|
26
|
+
//# sourceMappingURL=snapshot-state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot-state.d.ts","sourceRoot":"","sources":["../src/snapshot-state.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,UAAU,GACpB,MAAM,GAAG,SAAS,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;AAK7D,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;IACxD,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;CACzD,CAAC;AAMF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;IACtD,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;IACxD,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC,CAAC;AAIF,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,OAAO,GACb;IAAE,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAAE,CAO5D;AAID,wBAAgB,UAAU,CACxB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,OAAO,GACb;IACD,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAChC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACjC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACrB,CAuBA;AAGD,wBAAgB,MAAM,CAAC,KAAK,EAC1B,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,GACjC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAEvB;AASD,wBAAgB,aAAa,CAC3B,KAAK,EAAE,UAAU,GAAG,SAAS,GAC5B,MAAM,GAAG,SAAS,CAEpB"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { isPlainObject } from "./fingerprint.js";
|
|
2
|
+
// The previous snapshot and the keys the open scan's earlier parts listed,
|
|
3
|
+
// as diff wrote them. null when there is none.
|
|
4
|
+
export function readSnapshot(name, state) {
|
|
5
|
+
if (state === null)
|
|
6
|
+
return { snapshot: new Map(), listed: new Set() };
|
|
7
|
+
if (!isPlainObject(state))
|
|
8
|
+
throw invalid(name);
|
|
9
|
+
return {
|
|
10
|
+
snapshot: entries(name, state.snapshot),
|
|
11
|
+
listed: listedKeys(name, state.listed),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
// The previous groups, unknown keys and the groups the open scan's earlier
|
|
15
|
+
// parts listed, as diffGroups wrote them.
|
|
16
|
+
export function readGroups(name, state) {
|
|
17
|
+
if (state === null)
|
|
18
|
+
return { groups: new Map(), unknown: new Map(), listed: new Set() };
|
|
19
|
+
if (!isPlainObject(state) || !isPlainObject(state.groups))
|
|
20
|
+
throw invalid(name);
|
|
21
|
+
const groups = new Map();
|
|
22
|
+
for (const [key, group] of Object.entries(state.groups)) {
|
|
23
|
+
if (!isPlainObject(group) ||
|
|
24
|
+
(group.fingerprint !== null && typeof group.fingerprint !== 'string'))
|
|
25
|
+
throw invalid(name);
|
|
26
|
+
groups.set(key, {
|
|
27
|
+
fingerprint: group.fingerprint,
|
|
28
|
+
snapshot: Object.fromEntries(entries(name, group.snapshot)),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
groups,
|
|
33
|
+
unknown: state.unknown === undefined ? new Map() : entries(name, state.unknown),
|
|
34
|
+
listed: listedKeys(name, state.listed),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
// Sorted by key, so equal scans save byte-identical state.
|
|
38
|
+
export function sorted(values) {
|
|
39
|
+
return Object.fromEntries([...values].sort(([a], [b]) => compareKeys(a, b)));
|
|
40
|
+
}
|
|
41
|
+
// By UTF-16 code unit, not by locale, so the order never varies by machine.
|
|
42
|
+
function compareKeys(a, b) {
|
|
43
|
+
if (a < b)
|
|
44
|
+
return -1;
|
|
45
|
+
if (a > b)
|
|
46
|
+
return 1;
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
export function fingerprintIn(entry) {
|
|
50
|
+
return typeof entry === 'string' ? entry : entry?.[0];
|
|
51
|
+
}
|
|
52
|
+
function entries(name, snapshot) {
|
|
53
|
+
if (!isPlainObject(snapshot))
|
|
54
|
+
throw invalid(name);
|
|
55
|
+
const saved = new Map();
|
|
56
|
+
for (const [key, entry] of Object.entries(snapshot)) {
|
|
57
|
+
if (!isSavedEntry(entry))
|
|
58
|
+
throw invalid(name);
|
|
59
|
+
saved.set(key, entry);
|
|
60
|
+
}
|
|
61
|
+
return saved;
|
|
62
|
+
}
|
|
63
|
+
function listedKeys(name, listed) {
|
|
64
|
+
if (listed === undefined)
|
|
65
|
+
return new Set();
|
|
66
|
+
if (!Array.isArray(listed) || !listed.every((key) => typeof key === 'string'))
|
|
67
|
+
throw invalid(name);
|
|
68
|
+
return new Set(listed);
|
|
69
|
+
}
|
|
70
|
+
function isSavedEntry(value) {
|
|
71
|
+
return (typeof value === 'string' ||
|
|
72
|
+
(Array.isArray(value) &&
|
|
73
|
+
value.length === 2 &&
|
|
74
|
+
typeof value[0] === 'string' &&
|
|
75
|
+
typeof value[1] === 'string'));
|
|
76
|
+
}
|
|
77
|
+
function invalid(name) {
|
|
78
|
+
return new TypeError(`Snapshot ${name} state is not a saved snapshot`);
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=snapshot-state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot-state.js","sourceRoot":"","sources":["../src/snapshot-state.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AA+BjD,2EAA2E;AAC3E,+CAA+C;AAC/C,MAAM,UAAU,YAAY,CAC1B,IAAY,EACZ,KAAc;IAEd,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,GAAG,EAAE,EAAE,MAAM,EAAE,IAAI,GAAG,EAAE,EAAE,CAAC;IACtE,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC;QAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,OAAO;QACL,QAAQ,EAAE,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,QAAQ,CAAC;QACvC,MAAM,EAAE,UAAU,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC;KACvC,CAAC;AACJ,CAAC;AAED,2EAA2E;AAC3E,0CAA0C;AAC1C,MAAM,UAAU,UAAU,CACxB,IAAY,EACZ,KAAc;IAMd,IAAI,KAAK,KAAK,IAAI;QAChB,OAAO,EAAE,MAAM,EAAE,IAAI,GAAG,EAAE,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,EAAE,MAAM,EAAE,IAAI,GAAG,EAAE,EAAE,CAAC;IACtE,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,MAAM,CAAC;QACvD,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,MAAM,MAAM,GAAG,IAAI,GAAG,EAAsB,CAAC;IAC7C,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;QACxD,IACE,CAAC,aAAa,CAAC,KAAK,CAAC;YACrB,CAAC,KAAK,CAAC,WAAW,KAAK,IAAI,IAAI,OAAO,KAAK,CAAC,WAAW,KAAK,QAAQ,CAAC;YAErE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;QACtB,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE;YACd,WAAW,EAAE,KAAK,CAAC,WAAW;YAC9B,QAAQ,EAAE,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC;SAC5D,CAAC,CAAC;IACL,CAAC;IACD,OAAO;QACL,MAAM;QACN,OAAO,EACL,KAAK,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC;QACxE,MAAM,EAAE,UAAU,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC;KACvC,CAAC;AACJ,CAAC;AAED,2DAA2D;AAC3D,MAAM,UAAU,MAAM,CACpB,MAAkC;IAElC,OAAO,MAAM,CAAC,WAAW,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;AAC/E,CAAC;AAED,4EAA4E;AAC5E,SAAS,WAAW,CAAC,CAAS,EAAE,CAAS;IACvC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,CAAC,CAAC,CAAC;IACrB,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,CAAC,CAAC;IACpB,OAAO,CAAC,CAAC;AACX,CAAC;AAED,MAAM,UAAU,aAAa,CAC3B,KAA6B;IAE7B,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;AACxD,CAAC;AAED,SAAS,OAAO,CAAC,IAAY,EAAE,QAAiB;IAC9C,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC;QAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD,MAAM,KAAK,GAAG,IAAI,GAAG,EAAsB,CAAC;IAC5C,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QACpD,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC;YAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;QAC9C,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACxB,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,UAAU,CAAC,IAAY,EAAE,MAAe;IAC/C,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,IAAI,GAAG,EAAE,CAAC;IAC3C,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,GAAG,KAAK,QAAQ,CAAC;QAC3E,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,OAAO,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC;AACzB,CAAC;AAED,SAAS,YAAY,CAAC,KAAc;IAClC,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;YACnB,KAAK,CAAC,MAAM,KAAK,CAAC;YAClB,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,QAAQ;YAC5B,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,QAAQ,CAAC,CAChC,CAAC;AACJ,CAAC;AAED,SAAS,OAAO,CAAC,IAAY;IAC3B,OAAO,IAAI,SAAS,CAAC,YAAY,IAAI,gCAAgC,CAAC,CAAC;AACzE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timestamp.d.ts","sourceRoot":"","sources":["../src/timestamp.ts"],"names":[],"mappings":"AAEA,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAM3D"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// A UTC timestamp exactly as Date#toISOString writes it, so timestamps order
|
|
2
|
+
// as text.
|
|
3
|
+
export function isTimestamp(value) {
|
|
4
|
+
return (typeof value === 'string' &&
|
|
5
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value) &&
|
|
6
|
+
new Date(value).toISOString() === value);
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=timestamp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timestamp.js","sourceRoot":"","sources":["../src/timestamp.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,WAAW;AACX,MAAM,UAAU,WAAW,CAAC,KAAc;IACxC,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,+CAA+C,CAAC,IAAI,CAAC,KAAK,CAAC;QAC3D,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,KAAK,KAAK,CACxC,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zukhruf/snapshot-differential",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.3.25",
|
|
4
|
+
"description": "Change data capture for an upstream with no change log: compare each scan with the last snapshot, get the new and changed records, and let a deletion policy judge each key that the scan no longer lists.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/JanuaryLabs/zukhruf",
|
|
9
|
+
"directory": "packages/snapshot-differential"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/JanuaryLabs/zukhruf/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/JanuaryLabs/zukhruf/tree/main/packages/snapshot-differential#readme",
|
|
15
|
+
"keywords": [
|
|
16
|
+
"snapshot-differential",
|
|
17
|
+
"change-data-capture",
|
|
18
|
+
"cdc",
|
|
19
|
+
"diff",
|
|
20
|
+
"snapshot",
|
|
21
|
+
"incremental",
|
|
22
|
+
"etl",
|
|
23
|
+
"elt"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
26
|
+
"exports": {
|
|
27
|
+
"./package.json": "./package.json",
|
|
28
|
+
".": {
|
|
29
|
+
"@zukhruf/source": "./src/index.ts",
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"import": "./dist/index.js",
|
|
32
|
+
"default": "./dist/index.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"!dist/spec/**",
|
|
38
|
+
"!**/*.tsbuildinfo",
|
|
39
|
+
"!**/*.test.*"
|
|
40
|
+
],
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
}
|
|
44
|
+
}
|