@electric-sql/client 0.2.2
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 +177 -0
- package/README.md +80 -0
- package/dist/cjs/index.cjs +504 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/index.browser.mjs +2 -0
- package/dist/index.browser.mjs.map +1 -0
- package/dist/index.d.ts +243 -0
- package/dist/index.legacy-esm.js +450 -0
- package/dist/index.legacy-esm.js.map +1 -0
- package/dist/index.mjs +478 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +69 -0
- package/src/client.ts +572 -0
- package/src/index.ts +2 -0
- package/src/parser.ts +130 -0
- package/src/types.ts +108 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
type Value = string | number | boolean | bigint | null | Value[] | {
|
|
2
|
+
[key: string]: Value;
|
|
3
|
+
};
|
|
4
|
+
type Offset = `-1` | `${number}_${number}`;
|
|
5
|
+
interface Header {
|
|
6
|
+
[key: string]: Value;
|
|
7
|
+
}
|
|
8
|
+
type ControlMessage = {
|
|
9
|
+
headers: Header;
|
|
10
|
+
};
|
|
11
|
+
type ChangeMessage<T> = {
|
|
12
|
+
key: string;
|
|
13
|
+
value: T;
|
|
14
|
+
headers: Header & {
|
|
15
|
+
action: `insert` | `update` | `delete`;
|
|
16
|
+
};
|
|
17
|
+
offset: Offset;
|
|
18
|
+
};
|
|
19
|
+
type Message<T extends Value = {
|
|
20
|
+
[key: string]: Value;
|
|
21
|
+
}> = ControlMessage | ChangeMessage<T>;
|
|
22
|
+
type RegularColumn = {
|
|
23
|
+
type: string;
|
|
24
|
+
dims: number;
|
|
25
|
+
};
|
|
26
|
+
type VarcharColumn = {
|
|
27
|
+
type: `varchar`;
|
|
28
|
+
dims: number;
|
|
29
|
+
max_length?: number;
|
|
30
|
+
};
|
|
31
|
+
type BpcharColumn = {
|
|
32
|
+
type: `bpchar`;
|
|
33
|
+
dims: number;
|
|
34
|
+
length?: number;
|
|
35
|
+
};
|
|
36
|
+
type TimeColumn = {
|
|
37
|
+
type: `time` | `timetz` | `timestamp` | `timestamptz`;
|
|
38
|
+
dims: number;
|
|
39
|
+
precision?: number;
|
|
40
|
+
};
|
|
41
|
+
type IntervalColumn = {
|
|
42
|
+
type: `interval`;
|
|
43
|
+
dims: number;
|
|
44
|
+
fields?: `YEAR` | `MONTH` | `DAY` | `HOUR` | `MINUTE` | `YEAR TO MONTH` | `DAY TO HOUR` | `DAY TO MINUTE` | `DAY TO SECOND` | `HOUR TO MINUTE` | `HOUR TO SECOND` | `MINUTE TO SECOND`;
|
|
45
|
+
};
|
|
46
|
+
type IntervalColumnWithPrecision = {
|
|
47
|
+
type: `interval`;
|
|
48
|
+
dims: number;
|
|
49
|
+
precision?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
50
|
+
fields?: `SECOND`;
|
|
51
|
+
};
|
|
52
|
+
type BitColumn = {
|
|
53
|
+
type: `bit`;
|
|
54
|
+
dims: number;
|
|
55
|
+
length: number;
|
|
56
|
+
};
|
|
57
|
+
type NumericColumn = {
|
|
58
|
+
type: `numeric`;
|
|
59
|
+
dims: number;
|
|
60
|
+
precision?: number;
|
|
61
|
+
scale?: number;
|
|
62
|
+
};
|
|
63
|
+
type ColumnInfo = RegularColumn | VarcharColumn | BpcharColumn | TimeColumn | IntervalColumn | IntervalColumnWithPrecision | BitColumn | NumericColumn;
|
|
64
|
+
type Schema = {
|
|
65
|
+
[key: string]: ColumnInfo;
|
|
66
|
+
};
|
|
67
|
+
type TypedMessages<T extends Value = {
|
|
68
|
+
[key: string]: Value;
|
|
69
|
+
}> = {
|
|
70
|
+
messages: Array<Message<T>>;
|
|
71
|
+
schema: ColumnInfo;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
type ParseFunction = (value: string, additionalInfo?: Omit<ColumnInfo, `type` | `dims`>) => Value;
|
|
75
|
+
type Parser = {
|
|
76
|
+
[key: string]: ParseFunction;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
type ShapeData = Map<string, {
|
|
80
|
+
[key: string]: Value;
|
|
81
|
+
}>;
|
|
82
|
+
type ShapeChangedCallback = (value: ShapeData) => void;
|
|
83
|
+
interface BackoffOptions {
|
|
84
|
+
initialDelay: number;
|
|
85
|
+
maxDelay: number;
|
|
86
|
+
multiplier: number;
|
|
87
|
+
}
|
|
88
|
+
declare const BackoffDefaults: {
|
|
89
|
+
initialDelay: number;
|
|
90
|
+
maxDelay: number;
|
|
91
|
+
multiplier: number;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Options for constructing a ShapeStream.
|
|
95
|
+
*/
|
|
96
|
+
interface ShapeStreamOptions {
|
|
97
|
+
/**
|
|
98
|
+
* The full URL to where the Shape is hosted. This can either be the Electric server
|
|
99
|
+
* directly or a proxy. E.g. for a local Electric instance, you might set `http://localhost:3000/v1/shape/foo`
|
|
100
|
+
*/
|
|
101
|
+
url: string;
|
|
102
|
+
/**
|
|
103
|
+
* where clauses for the shape.
|
|
104
|
+
*/
|
|
105
|
+
where?: string;
|
|
106
|
+
/**
|
|
107
|
+
* The "offset" on the shape log. This is typically not set as the ShapeStream
|
|
108
|
+
* will handle this automatically. A common scenario where you might pass an offset
|
|
109
|
+
* is if you're maintaining a local cache of the log. If you've gone offline
|
|
110
|
+
* and are re-starting a ShapeStream to catch-up to the latest state of the Shape,
|
|
111
|
+
* you'd pass in the last offset and shapeId you'd seen from the Electric server
|
|
112
|
+
* so it knows at what point in the shape to catch you up from.
|
|
113
|
+
*/
|
|
114
|
+
offset?: Offset;
|
|
115
|
+
/**
|
|
116
|
+
* Similar to `offset`, this isn't typically used unless you're maintaining
|
|
117
|
+
* a cache of the shape log.
|
|
118
|
+
*/
|
|
119
|
+
shapeId?: string;
|
|
120
|
+
backoffOptions?: BackoffOptions;
|
|
121
|
+
/**
|
|
122
|
+
* Automatically fetch updates to the Shape. If you just want to sync the current
|
|
123
|
+
* shape and stop, pass false.
|
|
124
|
+
*/
|
|
125
|
+
subscribe?: boolean;
|
|
126
|
+
signal?: AbortSignal;
|
|
127
|
+
fetchClient?: typeof fetch;
|
|
128
|
+
parser?: Parser;
|
|
129
|
+
}
|
|
130
|
+
declare class FetchError extends Error {
|
|
131
|
+
url: string;
|
|
132
|
+
status: number;
|
|
133
|
+
text?: string;
|
|
134
|
+
json?: object;
|
|
135
|
+
headers: Record<string, string>;
|
|
136
|
+
constructor(status: number, text: string | undefined, json: object | undefined, headers: Record<string, string>, url: string, message?: string);
|
|
137
|
+
static fromResponse(response: Response, url: string): Promise<FetchError>;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Reads updates to a shape from Electric using HTTP requests and long polling. Notifies subscribers
|
|
141
|
+
* when new messages come in. Doesn't maintain any history of the
|
|
142
|
+
* log but does keep track of the offset position and is the best way
|
|
143
|
+
* to consume the HTTP `GET /v1/shape` api.
|
|
144
|
+
*
|
|
145
|
+
* @constructor
|
|
146
|
+
* @param {ShapeStreamOptions} options
|
|
147
|
+
*
|
|
148
|
+
* Register a callback function to subscribe to the messages.
|
|
149
|
+
*
|
|
150
|
+
* const stream = new ShapeStream(options)
|
|
151
|
+
* stream.subscribe(messages => {
|
|
152
|
+
* // messages is 1 or more row updates
|
|
153
|
+
* })
|
|
154
|
+
*
|
|
155
|
+
* To abort the stream, abort the `signal`
|
|
156
|
+
* passed in via the `ShapeStreamOptions`.
|
|
157
|
+
*
|
|
158
|
+
* const aborter = new AbortController()
|
|
159
|
+
* const issueStream = new ShapeStream({
|
|
160
|
+
* url: `${BASE_URL}/${table}`
|
|
161
|
+
* subscribe: true,
|
|
162
|
+
* signal: aborter.signal,
|
|
163
|
+
* })
|
|
164
|
+
* // Later...
|
|
165
|
+
* aborter.abort()
|
|
166
|
+
*/
|
|
167
|
+
declare class ShapeStream {
|
|
168
|
+
private options;
|
|
169
|
+
private backoffOptions;
|
|
170
|
+
private fetchClient;
|
|
171
|
+
private schema?;
|
|
172
|
+
private subscribers;
|
|
173
|
+
private upToDateSubscribers;
|
|
174
|
+
private lastOffset;
|
|
175
|
+
private messageParser;
|
|
176
|
+
isUpToDate: boolean;
|
|
177
|
+
shapeId?: string;
|
|
178
|
+
constructor(options: ShapeStreamOptions);
|
|
179
|
+
start(): Promise<void>;
|
|
180
|
+
subscribe(callback: (messages: Message[]) => void | Promise<void>, onError?: (error: FetchError | Error) => void): () => void;
|
|
181
|
+
unsubscribeAll(): void;
|
|
182
|
+
private publish;
|
|
183
|
+
private sendErrorToSubscribers;
|
|
184
|
+
subscribeOnceToUpToDate(callback: () => void | Promise<void>, error: (err: FetchError | Error) => void): () => void;
|
|
185
|
+
unsubscribeAllUpToDateSubscribers(): void;
|
|
186
|
+
private notifyUpToDateSubscribers;
|
|
187
|
+
private sendErrorToUpToDateSubscribers;
|
|
188
|
+
/**
|
|
189
|
+
* Resets the state of the stream, optionally with a provided
|
|
190
|
+
* shape ID
|
|
191
|
+
*/
|
|
192
|
+
private reset;
|
|
193
|
+
private validateOptions;
|
|
194
|
+
private fetchWithBackoff;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* A Shape is an object that subscribes to a shape log,
|
|
198
|
+
* keeps a materialised shape `.value` in memory and
|
|
199
|
+
* notifies subscribers when the value has changed.
|
|
200
|
+
*
|
|
201
|
+
* It can be used without a framework and as a primitive
|
|
202
|
+
* to simplify developing framework hooks.
|
|
203
|
+
*
|
|
204
|
+
* @constructor
|
|
205
|
+
* @param {Shape}
|
|
206
|
+
*
|
|
207
|
+
* const shapeStream = new ShapeStream(url: 'http://localhost:3000/v1/shape/foo'})
|
|
208
|
+
* const shape = new Shape(shapeStream)
|
|
209
|
+
*
|
|
210
|
+
* `value` returns a promise that resolves the Shape data once the Shape has been
|
|
211
|
+
* fully loaded (and when resuming from being offline):
|
|
212
|
+
*
|
|
213
|
+
* const value = await shape.value
|
|
214
|
+
*
|
|
215
|
+
* `valueSync` returns the current data synchronously:
|
|
216
|
+
*
|
|
217
|
+
* const value = shape.valueSync
|
|
218
|
+
*
|
|
219
|
+
* Subscribe to updates. Called whenever the shape updates in Postgres.
|
|
220
|
+
*
|
|
221
|
+
* shape.subscribe(shapeData => {
|
|
222
|
+
* console.log(shapeData)
|
|
223
|
+
* })
|
|
224
|
+
*/
|
|
225
|
+
declare class Shape {
|
|
226
|
+
private stream;
|
|
227
|
+
private data;
|
|
228
|
+
private subscribers;
|
|
229
|
+
error: FetchError | false;
|
|
230
|
+
private hasNotifiedSubscribersUpToDate;
|
|
231
|
+
constructor(stream: ShapeStream);
|
|
232
|
+
get isUpToDate(): boolean;
|
|
233
|
+
get value(): Promise<ShapeData>;
|
|
234
|
+
get valueSync(): ShapeData;
|
|
235
|
+
subscribe(callback: ShapeChangedCallback): () => void;
|
|
236
|
+
unsubscribeAll(): void;
|
|
237
|
+
get numSubscribers(): number;
|
|
238
|
+
private process;
|
|
239
|
+
private handleError;
|
|
240
|
+
private notify;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export { BackoffDefaults, type BackoffOptions, type BitColumn, type BpcharColumn, type ChangeMessage, type ColumnInfo, type ControlMessage, FetchError, type IntervalColumn, type IntervalColumnWithPrecision, type Message, type NumericColumn, type Offset, type RegularColumn, type Schema, Shape, type ShapeChangedCallback, type ShapeData, ShapeStream, type ShapeStreamOptions, type TimeColumn, type TypedMessages, type Value, type VarcharColumn };
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
3
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
4
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
5
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
6
|
+
var __spreadValues = (a, b) => {
|
|
7
|
+
for (var prop in b || (b = {}))
|
|
8
|
+
if (__hasOwnProp.call(b, prop))
|
|
9
|
+
__defNormalProp(a, prop, b[prop]);
|
|
10
|
+
if (__getOwnPropSymbols)
|
|
11
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
12
|
+
if (__propIsEnum.call(b, prop))
|
|
13
|
+
__defNormalProp(a, prop, b[prop]);
|
|
14
|
+
}
|
|
15
|
+
return a;
|
|
16
|
+
};
|
|
17
|
+
var __objRest = (source, exclude) => {
|
|
18
|
+
var target = {};
|
|
19
|
+
for (var prop in source)
|
|
20
|
+
if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
|
|
21
|
+
target[prop] = source[prop];
|
|
22
|
+
if (source != null && __getOwnPropSymbols)
|
|
23
|
+
for (var prop of __getOwnPropSymbols(source)) {
|
|
24
|
+
if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
|
|
25
|
+
target[prop] = source[prop];
|
|
26
|
+
}
|
|
27
|
+
return target;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// src/parser.ts
|
|
31
|
+
var parseNumber = (value) => Number(value);
|
|
32
|
+
var parseBool = (value) => value === `true` || value === `t`;
|
|
33
|
+
var parseBigInt = (value) => BigInt(value);
|
|
34
|
+
var parseJson = (value) => JSON.parse(value);
|
|
35
|
+
var defaultParser = {
|
|
36
|
+
int2: parseNumber,
|
|
37
|
+
int4: parseNumber,
|
|
38
|
+
int8: parseBigInt,
|
|
39
|
+
bool: parseBool,
|
|
40
|
+
float4: parseNumber,
|
|
41
|
+
float8: parseNumber,
|
|
42
|
+
json: parseJson,
|
|
43
|
+
jsonb: parseJson
|
|
44
|
+
};
|
|
45
|
+
function pgArrayParser(value, parser) {
|
|
46
|
+
let i = 0;
|
|
47
|
+
let char = null;
|
|
48
|
+
let str = ``;
|
|
49
|
+
let quoted = false;
|
|
50
|
+
let last = 0;
|
|
51
|
+
let p = void 0;
|
|
52
|
+
function loop(x) {
|
|
53
|
+
const xs = [];
|
|
54
|
+
for (; i < x.length; i++) {
|
|
55
|
+
char = x[i];
|
|
56
|
+
if (quoted) {
|
|
57
|
+
if (char === `\\`) {
|
|
58
|
+
str += x[++i];
|
|
59
|
+
} else if (char === `"`) {
|
|
60
|
+
xs.push(parser ? parser(str) : str);
|
|
61
|
+
str = ``;
|
|
62
|
+
quoted = x[i + 1] === `"`;
|
|
63
|
+
last = i + 2;
|
|
64
|
+
} else {
|
|
65
|
+
str += char;
|
|
66
|
+
}
|
|
67
|
+
} else if (char === `"`) {
|
|
68
|
+
quoted = true;
|
|
69
|
+
} else if (char === `{`) {
|
|
70
|
+
last = ++i;
|
|
71
|
+
xs.push(loop(x));
|
|
72
|
+
} else if (char === `}`) {
|
|
73
|
+
quoted = false;
|
|
74
|
+
last < i && xs.push(parser ? parser(x.slice(last, i)) : x.slice(last, i));
|
|
75
|
+
last = i + 1;
|
|
76
|
+
break;
|
|
77
|
+
} else if (char === `,` && p !== `}` && p !== `"`) {
|
|
78
|
+
xs.push(parser ? parser(x.slice(last, i)) : x.slice(last, i));
|
|
79
|
+
last = i + 1;
|
|
80
|
+
}
|
|
81
|
+
p = char;
|
|
82
|
+
}
|
|
83
|
+
last < i && xs.push(parser ? parser(x.slice(last, i + 1)) : x.slice(last, i + 1));
|
|
84
|
+
return xs;
|
|
85
|
+
}
|
|
86
|
+
return loop(value)[0];
|
|
87
|
+
}
|
|
88
|
+
var MessageParser = class {
|
|
89
|
+
constructor(parser) {
|
|
90
|
+
this.parser = __spreadValues(__spreadValues({}, defaultParser), parser);
|
|
91
|
+
}
|
|
92
|
+
parse(messages, schema) {
|
|
93
|
+
return JSON.parse(messages, (key, value) => {
|
|
94
|
+
if (key === `value` && typeof value === `object`) {
|
|
95
|
+
const row = value;
|
|
96
|
+
Object.keys(row).forEach((key2) => {
|
|
97
|
+
row[key2] = this.parseRow(key2, row[key2], schema);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return value;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
// Parses the message values using the provided parser based on the schema information
|
|
104
|
+
parseRow(key, value, schema) {
|
|
105
|
+
const columnInfo = schema[key];
|
|
106
|
+
if (!columnInfo) {
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
const parser = this.parser[columnInfo.type];
|
|
110
|
+
const _a = columnInfo, { type: _typ, dims: dimensions } = _a, additionalInfo = __objRest(_a, ["type", "dims"]);
|
|
111
|
+
if (dimensions > 0) {
|
|
112
|
+
const identityParser = (v) => v;
|
|
113
|
+
return pgArrayParser(value, parser != null ? parser : identityParser);
|
|
114
|
+
}
|
|
115
|
+
if (!parser) {
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
return parser(value, additionalInfo);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// src/client.ts
|
|
123
|
+
var BackoffDefaults = {
|
|
124
|
+
initialDelay: 100,
|
|
125
|
+
maxDelay: 1e4,
|
|
126
|
+
multiplier: 1.3
|
|
127
|
+
};
|
|
128
|
+
var MessageProcessor = class {
|
|
129
|
+
constructor(callback) {
|
|
130
|
+
this.messageQueue = [];
|
|
131
|
+
this.isProcessing = false;
|
|
132
|
+
this.callback = callback;
|
|
133
|
+
}
|
|
134
|
+
process(messages) {
|
|
135
|
+
this.messageQueue.push(messages);
|
|
136
|
+
if (!this.isProcessing) {
|
|
137
|
+
this.processQueue();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async processQueue() {
|
|
141
|
+
this.isProcessing = true;
|
|
142
|
+
while (this.messageQueue.length > 0) {
|
|
143
|
+
const messages = this.messageQueue.shift();
|
|
144
|
+
await this.callback(messages);
|
|
145
|
+
}
|
|
146
|
+
this.isProcessing = false;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
var FetchError = class _FetchError extends Error {
|
|
150
|
+
constructor(status, text, json, headers, url, message) {
|
|
151
|
+
super(
|
|
152
|
+
message || `HTTP Error ${status} at ${url}: ${text != null ? text : JSON.stringify(json)}`
|
|
153
|
+
);
|
|
154
|
+
this.url = url;
|
|
155
|
+
this.name = `FetchError`;
|
|
156
|
+
this.status = status;
|
|
157
|
+
this.text = text;
|
|
158
|
+
this.json = json;
|
|
159
|
+
this.headers = headers;
|
|
160
|
+
}
|
|
161
|
+
static async fromResponse(response, url) {
|
|
162
|
+
const status = response.status;
|
|
163
|
+
const headers = Object.fromEntries([...response.headers.entries()]);
|
|
164
|
+
let text = void 0;
|
|
165
|
+
let json = void 0;
|
|
166
|
+
const contentType = response.headers.get(`content-type`);
|
|
167
|
+
if (contentType && contentType.includes(`application/json`)) {
|
|
168
|
+
json = await response.json();
|
|
169
|
+
} else {
|
|
170
|
+
text = await response.text();
|
|
171
|
+
}
|
|
172
|
+
return new _FetchError(status, text, json, headers, url);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
var ShapeStream = class {
|
|
176
|
+
constructor(options) {
|
|
177
|
+
this.subscribers = /* @__PURE__ */ new Map();
|
|
178
|
+
this.upToDateSubscribers = /* @__PURE__ */ new Map();
|
|
179
|
+
this.isUpToDate = false;
|
|
180
|
+
var _a, _b, _c;
|
|
181
|
+
this.validateOptions(options);
|
|
182
|
+
this.options = __spreadValues({ subscribe: true }, options);
|
|
183
|
+
this.lastOffset = (_a = this.options.offset) != null ? _a : `-1`;
|
|
184
|
+
this.shapeId = this.options.shapeId;
|
|
185
|
+
this.messageParser = new MessageParser(options.parser);
|
|
186
|
+
this.backoffOptions = (_b = options.backoffOptions) != null ? _b : BackoffDefaults;
|
|
187
|
+
this.fetchClient = (_c = options.fetchClient) != null ? _c : (...args) => fetch(...args);
|
|
188
|
+
this.start();
|
|
189
|
+
}
|
|
190
|
+
async start() {
|
|
191
|
+
var _a, _b;
|
|
192
|
+
this.isUpToDate = false;
|
|
193
|
+
const { url, where, signal } = this.options;
|
|
194
|
+
while (!(signal == null ? void 0 : signal.aborted) && !this.isUpToDate || this.options.subscribe) {
|
|
195
|
+
const fetchUrl = new URL(url);
|
|
196
|
+
if (where) fetchUrl.searchParams.set(`where`, where);
|
|
197
|
+
fetchUrl.searchParams.set(`offset`, this.lastOffset);
|
|
198
|
+
if (this.isUpToDate) {
|
|
199
|
+
fetchUrl.searchParams.set(`live`, `true`);
|
|
200
|
+
}
|
|
201
|
+
if (this.shapeId) {
|
|
202
|
+
fetchUrl.searchParams.set(`shape_id`, this.shapeId);
|
|
203
|
+
}
|
|
204
|
+
let response;
|
|
205
|
+
try {
|
|
206
|
+
const maybeResponse = await this.fetchWithBackoff(fetchUrl);
|
|
207
|
+
if (maybeResponse) response = maybeResponse;
|
|
208
|
+
else break;
|
|
209
|
+
} catch (e) {
|
|
210
|
+
if (!(e instanceof FetchError)) throw e;
|
|
211
|
+
if (e.status == 409) {
|
|
212
|
+
const newShapeId = e.headers[`x-electric-shape-id`];
|
|
213
|
+
this.reset(newShapeId);
|
|
214
|
+
this.publish(e.json);
|
|
215
|
+
continue;
|
|
216
|
+
} else if (e.status >= 400 && e.status < 500) {
|
|
217
|
+
this.sendErrorToUpToDateSubscribers(e);
|
|
218
|
+
this.sendErrorToSubscribers(e);
|
|
219
|
+
throw e;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const { headers, status } = response;
|
|
223
|
+
const shapeId = headers.get(`X-Electric-Shape-Id`);
|
|
224
|
+
if (shapeId) {
|
|
225
|
+
this.shapeId = shapeId;
|
|
226
|
+
}
|
|
227
|
+
const lastOffset = headers.get(`X-Electric-Chunk-Last-Offset`);
|
|
228
|
+
if (lastOffset) {
|
|
229
|
+
this.lastOffset = lastOffset;
|
|
230
|
+
}
|
|
231
|
+
const getSchema = () => {
|
|
232
|
+
const schemaHeader = headers.get(`X-Electric-Schema`);
|
|
233
|
+
return schemaHeader ? JSON.parse(schemaHeader) : {};
|
|
234
|
+
};
|
|
235
|
+
this.schema = (_a = this.schema) != null ? _a : getSchema();
|
|
236
|
+
const messages = status === 204 ? `[]` : await response.text();
|
|
237
|
+
const batch = this.messageParser.parse(messages, this.schema);
|
|
238
|
+
if (batch.length > 0) {
|
|
239
|
+
const lastMessage = batch[batch.length - 1];
|
|
240
|
+
if (((_b = lastMessage.headers) == null ? void 0 : _b[`control`]) === `up-to-date` && !this.isUpToDate) {
|
|
241
|
+
this.isUpToDate = true;
|
|
242
|
+
this.notifyUpToDateSubscribers();
|
|
243
|
+
}
|
|
244
|
+
this.publish(batch);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
subscribe(callback, onError) {
|
|
249
|
+
const subscriptionId = Math.random();
|
|
250
|
+
const subscriber = new MessageProcessor(callback);
|
|
251
|
+
this.subscribers.set(subscriptionId, [subscriber, onError]);
|
|
252
|
+
return () => {
|
|
253
|
+
this.subscribers.delete(subscriptionId);
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
unsubscribeAll() {
|
|
257
|
+
this.subscribers.clear();
|
|
258
|
+
}
|
|
259
|
+
publish(messages) {
|
|
260
|
+
this.subscribers.forEach(([subscriber, _]) => {
|
|
261
|
+
subscriber.process(messages);
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
sendErrorToSubscribers(error) {
|
|
265
|
+
this.subscribers.forEach(([_, errorFn]) => {
|
|
266
|
+
errorFn == null ? void 0 : errorFn(error);
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
subscribeOnceToUpToDate(callback, error) {
|
|
270
|
+
const subscriptionId = Math.random();
|
|
271
|
+
this.upToDateSubscribers.set(subscriptionId, [callback, error]);
|
|
272
|
+
return () => {
|
|
273
|
+
this.upToDateSubscribers.delete(subscriptionId);
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
unsubscribeAllUpToDateSubscribers() {
|
|
277
|
+
this.upToDateSubscribers.clear();
|
|
278
|
+
}
|
|
279
|
+
notifyUpToDateSubscribers() {
|
|
280
|
+
this.upToDateSubscribers.forEach(([callback]) => {
|
|
281
|
+
callback();
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
sendErrorToUpToDateSubscribers(error) {
|
|
285
|
+
this.upToDateSubscribers.forEach(
|
|
286
|
+
([_, errorCallback]) => errorCallback(error)
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Resets the state of the stream, optionally with a provided
|
|
291
|
+
* shape ID
|
|
292
|
+
*/
|
|
293
|
+
reset(shapeId) {
|
|
294
|
+
this.lastOffset = `-1`;
|
|
295
|
+
this.shapeId = shapeId;
|
|
296
|
+
this.isUpToDate = false;
|
|
297
|
+
this.schema = void 0;
|
|
298
|
+
}
|
|
299
|
+
validateOptions(options) {
|
|
300
|
+
if (!options.url) {
|
|
301
|
+
throw new Error(`Invalid shape option. It must provide the url`);
|
|
302
|
+
}
|
|
303
|
+
if (options.signal && !(options.signal instanceof AbortSignal)) {
|
|
304
|
+
throw new Error(
|
|
305
|
+
`Invalid signal option. It must be an instance of AbortSignal.`
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
if (options.offset !== void 0 && options.offset !== `-1` && !options.shapeId) {
|
|
309
|
+
throw new Error(
|
|
310
|
+
`shapeId is required if this isn't an initial fetch (i.e. offset > -1)`
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
async fetchWithBackoff(url) {
|
|
315
|
+
const { initialDelay, maxDelay, multiplier } = this.backoffOptions;
|
|
316
|
+
const signal = this.options.signal;
|
|
317
|
+
let delay = initialDelay;
|
|
318
|
+
let attempt = 0;
|
|
319
|
+
while (true) {
|
|
320
|
+
try {
|
|
321
|
+
const result = await this.fetchClient(url.toString(), { signal });
|
|
322
|
+
if (result.ok) return result;
|
|
323
|
+
else throw await FetchError.fromResponse(result, url.toString());
|
|
324
|
+
} catch (e) {
|
|
325
|
+
if (signal == null ? void 0 : signal.aborted) {
|
|
326
|
+
return void 0;
|
|
327
|
+
} else if (e instanceof FetchError && e.status >= 400 && e.status < 500) {
|
|
328
|
+
throw e;
|
|
329
|
+
} else {
|
|
330
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
331
|
+
delay = Math.min(delay * multiplier, maxDelay);
|
|
332
|
+
attempt++;
|
|
333
|
+
console.log(`Retry attempt #${attempt} after ${delay}ms`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
var Shape = class {
|
|
340
|
+
constructor(stream) {
|
|
341
|
+
this.data = /* @__PURE__ */ new Map();
|
|
342
|
+
this.subscribers = /* @__PURE__ */ new Map();
|
|
343
|
+
this.error = false;
|
|
344
|
+
this.hasNotifiedSubscribersUpToDate = false;
|
|
345
|
+
this.stream = stream;
|
|
346
|
+
this.stream.subscribe(this.process.bind(this), this.handleError.bind(this));
|
|
347
|
+
const unsubscribe = this.stream.subscribeOnceToUpToDate(
|
|
348
|
+
() => {
|
|
349
|
+
unsubscribe();
|
|
350
|
+
},
|
|
351
|
+
(e) => {
|
|
352
|
+
this.handleError(e);
|
|
353
|
+
throw e;
|
|
354
|
+
}
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
get isUpToDate() {
|
|
358
|
+
return this.stream.isUpToDate;
|
|
359
|
+
}
|
|
360
|
+
get value() {
|
|
361
|
+
return new Promise((resolve) => {
|
|
362
|
+
if (this.stream.isUpToDate) {
|
|
363
|
+
resolve(this.valueSync);
|
|
364
|
+
} else {
|
|
365
|
+
const unsubscribe = this.stream.subscribeOnceToUpToDate(
|
|
366
|
+
() => {
|
|
367
|
+
unsubscribe();
|
|
368
|
+
resolve(this.valueSync);
|
|
369
|
+
},
|
|
370
|
+
(e) => {
|
|
371
|
+
throw e;
|
|
372
|
+
}
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
get valueSync() {
|
|
378
|
+
return this.data;
|
|
379
|
+
}
|
|
380
|
+
subscribe(callback) {
|
|
381
|
+
const subscriptionId = Math.random();
|
|
382
|
+
this.subscribers.set(subscriptionId, callback);
|
|
383
|
+
return () => {
|
|
384
|
+
this.subscribers.delete(subscriptionId);
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
unsubscribeAll() {
|
|
388
|
+
this.subscribers.clear();
|
|
389
|
+
}
|
|
390
|
+
get numSubscribers() {
|
|
391
|
+
return this.subscribers.size;
|
|
392
|
+
}
|
|
393
|
+
process(messages) {
|
|
394
|
+
let dataMayHaveChanged = false;
|
|
395
|
+
let isUpToDate = false;
|
|
396
|
+
let newlyUpToDate = false;
|
|
397
|
+
messages.forEach((message) => {
|
|
398
|
+
var _a, _b;
|
|
399
|
+
if (`key` in message) {
|
|
400
|
+
dataMayHaveChanged = [`insert`, `update`, `delete`].includes(
|
|
401
|
+
message.headers.action
|
|
402
|
+
);
|
|
403
|
+
switch (message.headers.action) {
|
|
404
|
+
case `insert`:
|
|
405
|
+
this.data.set(message.key, message.value);
|
|
406
|
+
break;
|
|
407
|
+
case `update`:
|
|
408
|
+
this.data.set(message.key, __spreadValues(__spreadValues({}, this.data.get(message.key)), message.value));
|
|
409
|
+
break;
|
|
410
|
+
case `delete`:
|
|
411
|
+
this.data.delete(message.key);
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (((_a = message.headers) == null ? void 0 : _a[`control`]) === `up-to-date`) {
|
|
416
|
+
isUpToDate = true;
|
|
417
|
+
if (!this.hasNotifiedSubscribersUpToDate) {
|
|
418
|
+
newlyUpToDate = true;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (((_b = message.headers) == null ? void 0 : _b[`control`]) === `must-refetch`) {
|
|
422
|
+
this.data.clear();
|
|
423
|
+
this.error = false;
|
|
424
|
+
isUpToDate = false;
|
|
425
|
+
newlyUpToDate = false;
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
if (newlyUpToDate || isUpToDate && dataMayHaveChanged) {
|
|
429
|
+
this.hasNotifiedSubscribersUpToDate = true;
|
|
430
|
+
this.notify();
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
handleError(e) {
|
|
434
|
+
if (e instanceof FetchError) {
|
|
435
|
+
this.error = e;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
notify() {
|
|
439
|
+
this.subscribers.forEach((callback) => {
|
|
440
|
+
callback(this.valueSync);
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
export {
|
|
445
|
+
BackoffDefaults,
|
|
446
|
+
FetchError,
|
|
447
|
+
Shape,
|
|
448
|
+
ShapeStream
|
|
449
|
+
};
|
|
450
|
+
//# sourceMappingURL=index.legacy-esm.js.map
|