@eltjs/elt 0.2.0 → 0.3.1

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 CHANGED
@@ -718,7 +718,7 @@ When a change signal fails:
718
718
 
719
719
  - A failed change signal fails only its streams. The other streams of the connection continue to load. A failed stream is not current as of the check.
720
720
  - The sync history records the failure one time, when the change signal starts to fail, as a failed pass of its streams with the failure type of the error.
721
- - After a `system` failure, such as a connection that its upstream dropped, the next check reads the gauge again, as Prometheus scrapes a failed target again at its next interval. A check that reads a new value loads the stream again.
721
+ - After a `system` failure, such as a connection that its upstream dropped, the next check reads the gauge again, as Prometheus scrapes a failed target again at its next interval. The first check that reads the gauge loads its streams again, also when the value did not change. Thus the sync history has a pass after the failed one, and a stream that failed for one check does not show as failed until its next change.
722
722
  - After a `config` failure, the watch reads that gauge no more, and its streams stay failed, because only the user can fix it. A new watch reads the gauge again.
723
723
  - An upkeep that fails is recorded in the same way, as a failed pass of the streams of the connection. After a `system` failure it runs again at its next interval. After a `config` failure it runs no more in this watch.
724
724
  - Change signals that break the contract stop their connection: a change signal names a stream that the connection did not select, a selected stream is in no change signal, the change signals of one stream have two intervals or two timeouts, or an interval or a timeout is not a positive number of milliseconds. A `changeSignals()` that throws also stops its connection. The sync history records it as a failed attempt. The other connections continue.
@@ -826,7 +826,7 @@ The destination contract:
826
826
 
827
827
  - Declarations are frozen, and you can use them again.
828
828
  - `Destination.createWriter(configuration, target)` selects a strategy of the storage with no I/O.
829
- - `Destination.load()` opens the one hold of the pass on the storage.
829
+ - `Destination.load({ clock, connection })` opens the one hold of the pass on the storage. `connection` is the name of the connection whose pass opens it, so a destination that records its loads can name each one.
830
830
  - `load.prepare(configuration, target, { writer, reloading })` gives each stream its `Stage`. A `Stage` has `apply`, `commit` and `discard`. It also has `fresh`, which is true when the target holds none of the earlier rows of the copy, so the copy reads from no checkpoint; `reloading`, which is true while commits go to a hidden target; and `complete`, which puts that hidden target in the place of the target after the stream ends with no failure.
831
831
  - Each destination uses `reloadMode` to decide what to do with the target: load into it, make it, evolve a stored target that the stream does not fit now ([shape changes](#shape-changes)), open a reload for an overwrite whose stored target does not fit now, or continue the reload that the last run of the copy left open. A `RESET` of the full stream also opens a reload.
832
832
  - Destinations own connections, files and publication. `Copy` and `Pipeline` have no SQL or file system code of a load.
@@ -1,4 +1,6 @@
1
1
  import type { CheckpointStore } from '../state/checkpoint-store.ts';
2
+ import type { PipelineClock } from './clock.ts';
3
+ import type { Connection } from './connection.ts';
2
4
  import { CopyConfiguration } from './copy-configuration.ts';
3
5
  import type { Destination } from './destination.ts';
4
6
  import type { Source } from './source.ts';
@@ -23,5 +25,5 @@ export declare class Copy<Target extends DestinationTarget> {
23
25
  get from(): Stream;
24
26
  writer(source: Source): string;
25
27
  validate(source: Source, destination: Destination<Target>, checkpoints?: CheckpointStore): void;
26
- clear(source: Source, destination: Destination<Target>, checkpoints?: CheckpointStore): Promise<void>;
28
+ clear({ name, source, destination, checkpoints }: Connection<Target>, clock: PipelineClock): Promise<void>;
27
29
  }
package/dist/core/copy.js CHANGED
@@ -44,10 +44,11 @@ export class Copy {
44
44
  }
45
45
  // Empties the target and removes this copy's checkpoint together, so the
46
46
  // next run reloads from scratch.
47
- async clear(source, destination, checkpoints) {
47
+ async clear({ name, source, destination, checkpoints }, clock) {
48
48
  this.validate(source, destination, checkpoints);
49
- const files = new FileTransfer(this.configuration.fileReads, destination.location(this.to), this.writer(source));
50
- const drop = () => destination.clear(this.configuration, this.to, this.writer(source), (values) => files.reconcile(values));
49
+ const writer = this.writer(source);
50
+ const files = new FileTransfer(this.configuration.fileReads, destination.location(this.to), writer);
51
+ const drop = () => destination.clear(this.configuration, this.to, { writer, clock, connection: name }, (values) => files.reconcile(values));
51
52
  if (this.id !== undefined && checkpoints !== undefined)
52
53
  await checkpoints.clear(this.id, drop);
53
54
  else
@@ -1,3 +1,4 @@
1
+ import type { PipelineClock } from './clock.ts';
1
2
  import type { Connection } from './connection.ts';
2
3
  import type { Target as DestinationTarget } from './target.ts';
3
4
  export type RefreshTrigger = {
@@ -17,6 +18,7 @@ export declare abstract class DerivedDataset<Target extends DestinationTarget> {
17
18
  validate(_connections: readonly Connection<Target>[]): void;
18
19
  abstract refresh(options: {
19
20
  readonly connections: readonly Connection<Target>[];
21
+ readonly clock: PipelineClock;
20
22
  readonly earlier: readonly DerivedDataset<Target>[];
21
23
  readonly signal?: AbortSignal;
22
24
  readonly trigger: RefreshTrigger;
@@ -1,10 +1,11 @@
1
+ import type { PipelineClock } from './clock.ts';
1
2
  import type { Connection } from './connection.ts';
2
3
  import { type DerivedDataset, type RefreshTrigger } from './derived-dataset.ts';
3
4
  import type { Target as DestinationTarget } from './target.ts';
4
5
  type Kind = RefreshTrigger['kind'];
5
6
  export declare class DerivedRefreshes<Target extends DestinationTarget> {
6
7
  #private;
7
- constructor(datasets: readonly DerivedDataset<Target>[], connections: readonly Connection<Target>[], signal: AbortSignal | undefined, stop: () => void);
8
+ constructor(datasets: readonly DerivedDataset<Target>[], connections: readonly Connection<Target>[], clock: PipelineClock, signal: AbortSignal | undefined, stop: () => void);
8
9
  get failure(): {
9
10
  readonly error: unknown;
10
11
  } | undefined;
@@ -40,6 +40,7 @@ class Failed {
40
40
  export class DerivedRefreshes {
41
41
  #datasets;
42
42
  #connections;
43
+ #clock;
43
44
  #signal;
44
45
  #stop;
45
46
  #state = new Idle();
@@ -47,9 +48,10 @@ export class DerivedRefreshes {
47
48
  // refresh; the memory of the passes that run, which ends with them.
48
49
  #tried = new Map();
49
50
  #health = new Healthy();
50
- constructor(datasets, connections, signal, stop) {
51
+ constructor(datasets, connections, clock, signal, stop) {
51
52
  this.#datasets = datasets;
52
53
  this.#connections = connections;
54
+ this.#clock = clock;
53
55
  this.#signal = signal;
54
56
  this.#stop = stop;
55
57
  }
@@ -88,6 +90,7 @@ export class DerivedRefreshes {
88
90
  try {
89
91
  await dataset.refresh({
90
92
  connections: this.#connections,
93
+ clock: this.#clock,
91
94
  earlier: this.#datasets.slice(0, index),
92
95
  signal: this.#signal,
93
96
  trigger: { kind, tried },
@@ -1,7 +1,7 @@
1
1
  import type { PipelineClock } from './clock.ts';
2
2
  import type { CopyConfiguration } from './copy-configuration.ts';
3
3
  import type { Target as DestinationTarget } from './target.ts';
4
- import type { FieldValues, Stage, Writer } from './writer.ts';
4
+ import type { Clearing, FieldValues, Stage, Writer } from './writer.ts';
5
5
  export type DestinationSyncMode = 'append' | 'overwrite' | 'append_dedup' | 'overwrite_dedup';
6
6
  export type Load<Target extends DestinationTarget> = AsyncDisposable & {
7
7
  prepare(configuration: CopyConfiguration, target: Target, binding: {
@@ -17,6 +17,7 @@ export declare abstract class Destination<Target extends DestinationTarget> {
17
17
  abstract createWriter(configuration: CopyConfiguration, target: Target): Writer;
18
18
  abstract load(options: {
19
19
  clock: PipelineClock;
20
+ connection: string;
20
21
  }): Promise<Load<Target>>;
21
- clear(configuration: CopyConfiguration, target: Target, writer: string, committed?: (values: FieldValues) => Promise<void>): Promise<void>;
22
+ clear(configuration: CopyConfiguration, target: Target, clearing: Clearing, committed?: (values: FieldValues) => Promise<void>): Promise<void>;
22
23
  }
@@ -11,7 +11,7 @@ export class Destination {
11
11
  if (!this.supportedDestinationSyncModes.includes(configuration.destinationSyncMode))
12
12
  throw new TypeError(`Destination does not support ${configuration.destinationSyncMode}`);
13
13
  }
14
- async clear(configuration, target, writer, committed) {
15
- await this.createWriter(configuration, target).clear(writer, committed);
14
+ async clear(configuration, target, clearing, committed) {
15
+ await this.createWriter(configuration, target).clear(clearing, committed);
16
16
  }
17
17
  }
@@ -72,7 +72,7 @@ export class Pipeline {
72
72
  const connection = this.connections.find(({ steps }) => steps.includes(copy));
73
73
  if (connection === undefined)
74
74
  throw new TypeError("Only this pipeline's copies can be cleared");
75
- await copy.clear(connection.source, connection.destination, connection.checkpoints);
75
+ await copy.clear(connection, this.#clock);
76
76
  }
77
77
  }
78
78
  // Once signal aborts, each pass stops at its next message and is recorded as
@@ -87,7 +87,7 @@ export class Pipeline {
87
87
  async run(options) {
88
88
  const signal = options?.signal;
89
89
  await this.#declare();
90
- const refreshes = new DerivedRefreshes(this.derived, this.connections, signal, () => { });
90
+ const refreshes = new DerivedRefreshes(this.derived, this.connections, this.#clock, signal, () => { });
91
91
  let passes;
92
92
  try {
93
93
  passes = await Promise.allSettled(this.connections.map((connection) => this.#pass(connection, connection.steps, signal, () => refreshes.request('commit'))));
@@ -128,7 +128,7 @@ export class Pipeline {
128
128
  const controller = new AbortController();
129
129
  const watching = AbortSignal.any([signal, controller.signal]);
130
130
  const stopped = [];
131
- const refreshes = new DerivedRefreshes(this.derived, this.connections, watching,
131
+ const refreshes = new DerivedRefreshes(this.derived, this.connections, this.#clock, watching,
132
132
  // With no reason: the passes it stops were not refused by this failure,
133
133
  // and no sync history records a dataset's failure (ADR 0004).
134
134
  () => controller.abort());
@@ -278,8 +278,12 @@ export class Pipeline {
278
278
  }
279
279
  continue;
280
280
  }
281
- failing.delete(name);
282
- if (outcome.state === 'changed') {
281
+ // A stream whose change signal failed loads again when a check
282
+ // reads the signal again, also with no new value: the history
283
+ // shows the latest attempt of a stream, and only a pass follows
284
+ // the failed one (ADR 0020).
285
+ const recovered = failing.delete(name);
286
+ if (outcome.state === 'changed' || recovered) {
283
287
  unchangedAt.delete(name);
284
288
  pending.add(name);
285
289
  }
@@ -99,9 +99,9 @@ export async function replicate(source, destination, checkpoints, copies, connec
99
99
  });
100
100
  try {
101
101
  if (checkpoints === undefined || bindings.size === 0)
102
- await transfer(source, destination, null, replications, signal, clock);
102
+ await transfer(source, destination, connection, null, replications, signal, clock);
103
103
  else
104
- await checkpoints.run(bindings, (run) => transfer(source, destination, run, replications, signal, clock));
104
+ await checkpoints.run(bindings, (run) => transfer(source, destination, connection, run, replications, signal, clock));
105
105
  }
106
106
  catch (error) {
107
107
  for (const replication of replications)
@@ -115,7 +115,7 @@ export async function replicate(source, destination, checkpoints, copies, connec
115
115
  replication.settle();
116
116
  return replications.map((replication) => replication.outcome());
117
117
  }
118
- async function transfer(source, destination, run, replications, signal, clock) {
118
+ async function transfer(source, destination, connection, run, replications, signal, clock) {
119
119
  const byStream = new Map(replications.map((replication) => [replication.stream.name, replication]));
120
120
  const incremental = (replication) => replication.copy.configuration.syncMode === 'incremental';
121
121
  const checkpoint = (replication) => {
@@ -153,7 +153,10 @@ async function transfer(source, destination, run, replications, signal, clock) {
153
153
  if (reading.length === 0)
154
154
  return;
155
155
  signal?.throwIfAborted();
156
- await using load = await destination.load({ clock });
156
+ await using load = await destination.load({
157
+ clock,
158
+ connection: connection.name,
159
+ });
157
160
  // Every target is prepared before anything is read; a copy whose target is
158
161
  // refused reads nothing.
159
162
  const prepared = [];
@@ -1,3 +1,4 @@
1
+ import type { PipelineClock } from './clock.ts';
1
2
  import type { Partition } from './partition.ts';
2
3
  import type { FailureType, KeyValue } from './source.ts';
3
4
  import type { Stream } from './stream.ts';
@@ -37,5 +38,10 @@ export type Stage = AsyncDisposable & {
37
38
  export declare abstract class Writer {
38
39
  readonly stream: Stream;
39
40
  constructor(stream: Stream);
40
- abstract clear(writer: string, committed?: (values: FieldValues) => Promise<void>): Promise<void>;
41
+ abstract clear(clearing: Clearing, committed?: (values: FieldValues) => Promise<void>): Promise<void>;
41
42
  }
43
+ export type Clearing = {
44
+ readonly writer: string;
45
+ readonly clock: PipelineClock;
46
+ readonly connection: string;
47
+ };
package/dist/index.d.ts CHANGED
@@ -33,7 +33,7 @@ export { type Upkeep } from './core/upkeep.ts';
33
33
  export { Stream, type SyncMode } from './core/stream.ts';
34
34
  export { Target } from './core/target.ts';
35
35
  export { type DescribedColumn, describeTarget, type TargetDescription, } from './core/target-description.ts';
36
- export { type FieldValues, type LoadFailure, type Stage, TargetOwnedError, type WriteCount, type WriteOperation, Writer, } from './core/writer.ts';
36
+ export { type Clearing, type FieldValues, type LoadFailure, type Stage, TargetOwnedError, type WriteCount, type WriteOperation, Writer, } from './core/writer.ts';
37
37
  export { type CheckpointBinding, type CheckpointRun, type CheckpointSession, CheckpointStore, type StoredCheckpoint, } from './state/checkpoint-store.ts';
38
38
  export { copyStatus, type DeclaredCopy, errorMessage, passError, passFailureType, passStatus, type RecordedPass, recordedFailure, type StreamCheck, SyncHistory, type SyncStatus, } from './state/sync-history.ts';
39
39
  export { syncHistoryRelations } from './state/sync-history-relations.ts';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eltjs/elt",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "A pipeline that copies the data of apps and services into stores that SQL can read: sources, streams, checkpoints, watches and derived datasets.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {