@rig-ts/electric 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,203 @@
1
+ import * as _$_tanstack_db0 from "@tanstack/db";
2
+ import * as _$_tanstack_electric_db_collection0 from "@tanstack/electric-db-collection";
3
+ import { Row, ShapeStreamOptions } from "@electric-sql/client";
4
+ import { Runtime } from "@rig-ts/client";
5
+
6
+ //#region src/params.d.ts
7
+ /** Query params the sync protocol accepts alongside its own. */
8
+ type ShapeParams = NonNullable<ShapeStreamOptions["params"]>;
9
+ /**
10
+ * The TypeScript types a declared shape param can take.
11
+ *
12
+ * UUID, Date, Time and Timestamp are all carried as strings, matching what the
13
+ * generated params types declare and what the server parses off the query
14
+ * string.
15
+ */
16
+ type ParamValue = string | number | boolean;
17
+ /**
18
+ * Serializes a shape's declared params into the query string the generated
19
+ * server parses.
20
+ *
21
+ * `undefined` values are dropped rather than sent empty — that is what makes an
22
+ * optional param absent, since the server treats an empty value as unset and an
23
+ * absent one as not asked for.
24
+ */
25
+ declare function serializeParams(params: Readonly<Record<string, ParamValue | undefined>>): ShapeParams;
26
+ /**
27
+ * A stable key for a param set, sorted so two callers passing the same params in
28
+ * a different literal order share one collection.
29
+ *
30
+ * Built on {@link serializeParams} and `URLSearchParams` rather than on a hand
31
+ * rolled `join("&")`, for the reason a hand rolled one cannot be right: a value
32
+ * containing the separators is indistinguishable from more params. `{a: "b&c=d"}`
33
+ * and `{a: "b", c: "d"}` produced the identical key, so two collections over
34
+ * different params shared one instance — and the rows one of them was asking for
35
+ * were never the rows it got. Percent-encoding is what removes the ambiguity.
36
+ *
37
+ * The sort is `URLSearchParams.sort`, which orders by code unit rather than by
38
+ * locale. Only stability matters here — nobody reads this key — and a key that
39
+ * depends on the reader's locale is the weaker of the two.
40
+ */
41
+ declare function paramsCacheKey(params: Readonly<Record<string, ParamValue | undefined>>): string;
42
+ //#endregion
43
+ //#region src/create-collection.d.ts
44
+ /** What a generated stream factory passes down. */
45
+ type RigCollectionArgs<TRow extends Row> = {
46
+ /**
47
+ * The client the stream authenticates and resolves its URL through. Taking
48
+ * the whole runtime rather than a base URL is what lets a `Session` refresh
49
+ * before a long poll inherits a token about to expire.
50
+ */
51
+ runtime: Runtime;
52
+ /**
53
+ * The shape's route, emitted verbatim from `ir.ElectricEndpoint` — for
54
+ * example `/api/v1/todo/_stream`. It is the full route including the API's
55
+ * base path, because that is what the document says it is; nothing here
56
+ * recomposes one.
57
+ */
58
+ path: string; /** The params the endpoint declares. Absent ones are dropped, not sent empty. */
59
+ params?: Readonly<Record<string, ParamValue | undefined>>; /** The primary key accessor. TanStack DB keys must be a string or a number. */
60
+ getKey: (row: TRow) => string | number;
61
+ };
62
+ /**
63
+ * Builds a read-only collection over one of a rig application's shape endpoints.
64
+ *
65
+ * **No mutation handlers.** Writes go through the API and the sync service
66
+ * delivers the resulting rows, so there is no txid handshake to perform here. A
67
+ * collection that could be written to would be a second way to change a row, and
68
+ * one that skips every rule the server applies.
69
+ *
70
+ * **No schema.** TanStack DB validates only on optimistic mutations, so a schema
71
+ * here would never run; the row types come from the generated API types, which
72
+ * are the ones the server sends.
73
+ *
74
+ * Sync does not start when this is called. It begins when the first live query
75
+ * subscribes and pauses when the last one unsubscribes, so an instance held
76
+ * across a navigation resumes rather than re-syncing — which is what
77
+ * {@link createCollectionCache} exists to make possible.
78
+ */
79
+ declare function createRigCollection<TRow extends Row>(args: RigCollectionArgs<TRow>): _$_tanstack_db0.Collection<TRow, string | number, _$_tanstack_electric_db_collection0.ElectricCollectionUtils<TRow>, never, TRow> & _$_tanstack_db0.NonSingleResult;
80
+ //#endregion
81
+ //#region src/collection-cache.d.ts
82
+ /** The slice of a collection's surface the cache needs to observe. */
83
+ type CacheableCollection = {
84
+ on: (event: "status:change", callback: (payload: {
85
+ status: string;
86
+ }) => void) => () => void;
87
+ };
88
+ /**
89
+ * Wraps a collection factory so each distinct client and param set maps to one
90
+ * long-lived instance.
91
+ *
92
+ * Collections are stateful and each one opens a stream, so an instance rebuilt
93
+ * per render or per navigation loses everything the library gives a retained
94
+ * one: data held for its collection time, sync that pauses at zero subscribers
95
+ * and resumes on the next one, and resume-from-offset instead of a full
96
+ * re-sync. Returning the same instance is what makes those pay off — and it is
97
+ * what lets a caller invoke a generated factory during render without a
98
+ * load-bearing `useMemo`.
99
+ *
100
+ * Entries remove themselves once their collection reaches `cleaned-up`, so the
101
+ * map is bounded by what is currently live rather than by everything ever asked
102
+ * for. There is no eviction policy to tune.
103
+ *
104
+ * On the server every call builds a fresh instance. The map would otherwise be
105
+ * module-global and leak one request's collections into the next, and nothing
106
+ * server-side should be syncing anyway.
107
+ *
108
+ * Two things the key deliberately leaves out. The **path** is not in it, because
109
+ * each generated factory wraps its own call to this and so has a map of its own
110
+ * — a caller wrapping two different routes in one cache would collide, which is
111
+ * why the generator does not. And the **credential** is not in it: a client that
112
+ * switches to a different tenant on the same origin keeps the collection it had.
113
+ * The rows it holds were scoped to the old session, so switching tenant should
114
+ * discard the collection rather than expect this to notice.
115
+ */
116
+ declare function createCollectionCache<TParams extends Readonly<Record<string, ParamValue | undefined>>, TCollection extends CacheableCollection>(build: (runtime: Runtime, params: TParams) => TCollection): (runtime: Runtime, params: TParams) => TCollection;
117
+ //#endregion
118
+ //#region src/fetch-client.d.ts
119
+ /**
120
+ * The fetch every stream goes out through.
121
+ *
122
+ * Two things it does that a bare `fetch` would not.
123
+ *
124
+ * **The credential.** rig authenticates with `Authorization: Bearer`, so a shape
125
+ * request carries the same credential every other call does — and a `Session`
126
+ * gets to refresh ahead of an expiry before a long poll inherits a token that
127
+ * will die halfway through it. This is the whole reason a collection takes a
128
+ * `Runtime` rather than a base URL.
129
+ *
130
+ * **`cache: "no-store"`.** Without it the browser serves a stale long-poll
131
+ * response back, and the subscription stops advancing while appearing to work.
132
+ *
133
+ * A cross-origin deployment pays for the header: `Authorization` is not
134
+ * CORS-safelisted, so the shape GET becomes a preflighted request, and whatever
135
+ * sits in front has to allow it *and* expose `electric-handle`,
136
+ * `electric-offset`, `electric-schema` and `electric-cursor` — the cursor the
137
+ * client resumes from. Same-origin, which is what rig serves by default, needs
138
+ * none of that.
139
+ */
140
+ declare function rigFetchClient(runtime: Runtime): typeof fetch;
141
+ //#endregion
142
+ //#region src/on-error.d.ts
143
+ type ErrorHandler = NonNullable<ShapeStreamOptions["onError"]>;
144
+ /**
145
+ * Routes stream failures back into the client's own auth handling.
146
+ *
147
+ * The sync client has already exhausted its own retries for 5xx, network errors
148
+ * and 429 by the time this runs, so what reaches here is a failure it will not
149
+ * retry on its own. Returning an object retries with backoff; returning
150
+ * `undefined` stops the stream for good.
151
+ *
152
+ * A 401 asks the credential to refresh — once, and only a credential that can do
153
+ * something about it. This is the counterpart of what the REST path does on a
154
+ * 401, and it has to happen here because a long poll cannot be re-sent by the
155
+ * caller: an expired session mid-stream otherwise stalls silently, polling into
156
+ * a session that is not coming back.
157
+ *
158
+ * Any other 4xx is a decision the server already made, and retrying cannot
159
+ * change it. A shape refused for the caller's tenant is refused.
160
+ */
161
+ declare function streamErrorHandler(runtime: Runtime): ErrorHandler;
162
+ //#endregion
163
+ //#region src/parsers.d.ts
164
+ type Parser = NonNullable<ShapeStreamOptions["parser"]>;
165
+ /**
166
+ * Wire-format corrections applied to every stream.
167
+ *
168
+ * The rule they all serve: a column reaching the client over REST and the same
169
+ * column reaching it over a stream must decode to the same value. One row
170
+ * arriving two ways and disagreeing with itself is the failure mode a generated
171
+ * client exists to prevent, and it is the one the sync protocol makes easy —
172
+ * REST answers with what Go's `encoding/json` wrote, and a stream answers with
173
+ * what Postgres printed.
174
+ *
175
+ * `date` and `time` deliberately have no entry: Postgres already writes them as
176
+ * `YYYY-MM-DD` and `HH:MM:SS`, which is what the REST path sends too. `numeric`
177
+ * likewise stays a string, because that is the only form that keeps its
178
+ * precision.
179
+ */
180
+ declare const rigParsers: Parser;
181
+ //#endregion
182
+ //#region src/shape-url.d.ts
183
+ /**
184
+ * The address one stream subscribes to.
185
+ *
186
+ * It exists because the sync client resolves nothing: it hands its `url` to
187
+ * `new URL()` with no base. So the relative origin `@rig-ts/client` documents as
188
+ * the ordinary same-origin case — `baseUrl: ""`, resolved against the page —
189
+ * arrives there as `/api/v1/todo/_stream` and throws `Invalid URL`. And because
190
+ * a TypeError is not a `FetchError`, the error handler reads it as a failure
191
+ * worth another go and the stream retries the same unusable address with
192
+ * backoff, silently, rather than reporting anything. Resolving it here is what
193
+ * makes the documented default work for a stream as well as for a REST call,
194
+ * where `fetch` does this much itself.
195
+ *
196
+ * Off a browser there is no page to resolve against, so a relative origin is
197
+ * left as it is. Nothing should be syncing during a server render, and a stream
198
+ * that starts anyway should fail naming the origin it was not given rather than
199
+ * one this invented for it.
200
+ */
201
+ declare function shapeUrl(origin: string, path: string): string;
202
+ //#endregion
203
+ export { type ParamValue, type RigCollectionArgs, type ShapeParams, createCollectionCache, createRigCollection, paramsCacheKey, rigFetchClient, rigParsers, serializeParams, shapeUrl, streamErrorHandler };
package/dist/index.mjs ADDED
@@ -0,0 +1,273 @@
1
+ import { createCollection } from "@tanstack/db";
2
+ import { electricCollectionOptions } from "@tanstack/electric-db-collection";
3
+ import { FetchError } from "@electric-sql/client";
4
+ import { isReauthorizer } from "@rig-ts/client";
5
+ //#region src/fetch-client.ts
6
+ /**
7
+ * The fetch every stream goes out through.
8
+ *
9
+ * Two things it does that a bare `fetch` would not.
10
+ *
11
+ * **The credential.** rig authenticates with `Authorization: Bearer`, so a shape
12
+ * request carries the same credential every other call does — and a `Session`
13
+ * gets to refresh ahead of an expiry before a long poll inherits a token that
14
+ * will die halfway through it. This is the whole reason a collection takes a
15
+ * `Runtime` rather than a base URL.
16
+ *
17
+ * **`cache: "no-store"`.** Without it the browser serves a stale long-poll
18
+ * response back, and the subscription stops advancing while appearing to work.
19
+ *
20
+ * A cross-origin deployment pays for the header: `Authorization` is not
21
+ * CORS-safelisted, so the shape GET becomes a preflighted request, and whatever
22
+ * sits in front has to allow it *and* expose `electric-handle`,
23
+ * `electric-offset`, `electric-schema` and `electric-cursor` — the cursor the
24
+ * client resumes from. Same-origin, which is what rig serves by default, needs
25
+ * none of that.
26
+ */
27
+ function rigFetchClient(runtime) {
28
+ return async (input, init) => {
29
+ const headers = new Headers(init?.headers);
30
+ await runtime.getCredential()?.apply(headers);
31
+ return await runtime.fetch(input, {
32
+ ...init,
33
+ headers,
34
+ cache: "no-store"
35
+ });
36
+ };
37
+ }
38
+ //#endregion
39
+ //#region src/on-error.ts
40
+ /**
41
+ * Routes stream failures back into the client's own auth handling.
42
+ *
43
+ * The sync client has already exhausted its own retries for 5xx, network errors
44
+ * and 429 by the time this runs, so what reaches here is a failure it will not
45
+ * retry on its own. Returning an object retries with backoff; returning
46
+ * `undefined` stops the stream for good.
47
+ *
48
+ * A 401 asks the credential to refresh — once, and only a credential that can do
49
+ * something about it. This is the counterpart of what the REST path does on a
50
+ * 401, and it has to happen here because a long poll cannot be re-sent by the
51
+ * caller: an expired session mid-stream otherwise stalls silently, polling into
52
+ * a session that is not coming back.
53
+ *
54
+ * Any other 4xx is a decision the server already made, and retrying cannot
55
+ * change it. A shape refused for the caller's tenant is refused.
56
+ */
57
+ function streamErrorHandler(runtime) {
58
+ let refreshed = false;
59
+ return async (error) => {
60
+ if (typeof window === "undefined") return void 0;
61
+ if (!(error instanceof FetchError)) return {};
62
+ if (error.status === 401 && !refreshed) {
63
+ refreshed = true;
64
+ const credential = runtime.getCredential();
65
+ if (isReauthorizer(credential) && await credential.reauthorize()) return {};
66
+ return;
67
+ }
68
+ if (error.status >= 400 && error.status < 500) return void 0;
69
+ return {};
70
+ };
71
+ }
72
+ //#endregion
73
+ //#region src/params.ts
74
+ /**
75
+ * Serializes a shape's declared params into the query string the generated
76
+ * server parses.
77
+ *
78
+ * `undefined` values are dropped rather than sent empty — that is what makes an
79
+ * optional param absent, since the server treats an empty value as unset and an
80
+ * absent one as not asked for.
81
+ */
82
+ function serializeParams(params) {
83
+ const out = {};
84
+ for (const [name, value] of Object.entries(params)) {
85
+ if (value === void 0) continue;
86
+ out[name] = String(value);
87
+ }
88
+ return out;
89
+ }
90
+ /**
91
+ * A stable key for a param set, sorted so two callers passing the same params in
92
+ * a different literal order share one collection.
93
+ *
94
+ * Built on {@link serializeParams} and `URLSearchParams` rather than on a hand
95
+ * rolled `join("&")`, for the reason a hand rolled one cannot be right: a value
96
+ * containing the separators is indistinguishable from more params. `{a: "b&c=d"}`
97
+ * and `{a: "b", c: "d"}` produced the identical key, so two collections over
98
+ * different params shared one instance — and the rows one of them was asking for
99
+ * were never the rows it got. Percent-encoding is what removes the ambiguity.
100
+ *
101
+ * The sort is `URLSearchParams.sort`, which orders by code unit rather than by
102
+ * locale. Only stability matters here — nobody reads this key — and a key that
103
+ * depends on the reader's locale is the weaker of the two.
104
+ */
105
+ function paramsCacheKey(params) {
106
+ const q = new URLSearchParams(Object.entries(serializeParams(params)).map(([name, value]) => [name, String(value)]));
107
+ q.sort();
108
+ return q.toString();
109
+ }
110
+ //#endregion
111
+ //#region src/parsers.ts
112
+ /**
113
+ * Wire-format corrections applied to every stream.
114
+ *
115
+ * The rule they all serve: a column reaching the client over REST and the same
116
+ * column reaching it over a stream must decode to the same value. One row
117
+ * arriving two ways and disagreeing with itself is the failure mode a generated
118
+ * client exists to prevent, and it is the one the sync protocol makes easy —
119
+ * REST answers with what Go's `encoding/json` wrote, and a stream answers with
120
+ * what Postgres printed.
121
+ *
122
+ * `date` and `time` deliberately have no entry: Postgres already writes them as
123
+ * `YYYY-MM-DD` and `HH:MM:SS`, which is what the REST path sends too. `numeric`
124
+ * likewise stays a string, because that is the only form that keeps its
125
+ * precision.
126
+ */
127
+ const rigParsers = {
128
+ int8: parseInt8,
129
+ timestamptz: toRFC3339,
130
+ timestamp: toRFC3339
131
+ };
132
+ /**
133
+ * Converts a Postgres timestamp to the RFC 3339 the REST path sends.
134
+ *
135
+ * Two corrections, and both matter. Postgres separates the date and the time
136
+ * with a space, which no ISO parser accepts. And it writes a zone offset as
137
+ * `+00` where RFC 3339 wants `Z` or `+00:00` — the shorter form parses nowhere
138
+ * reliably, and `Date.parse` reading it as local time would shift the value by
139
+ * the viewer's offset without failing.
140
+ *
141
+ * A `timestamp` — the wall-clock type, with no zone at all — is given `Z`. That
142
+ * asserts the column is UTC, which is the same assertion the REST path already
143
+ * makes: pgx reads a zone-less timestamp into a `time.Time` in UTC, and Go
144
+ * marshals that with the suffix. rig's own columns are `timestamptz` by
145
+ * convention, so this covers a hand-written column rather than anything rig
146
+ * generates.
147
+ */
148
+ function toRFC3339(value) {
149
+ const isoish = value.replace(" ", "T");
150
+ const shortOffset = /([+-]\d{2})$/.exec(isoish);
151
+ if (shortOffset !== null) return (shortOffset[1] ?? "") === "+00" ? `${isoish.slice(0, -3)}Z` : `${isoish}:00`;
152
+ if (/([+-]\d{2}:\d{2}|Z)$/.test(isoish)) return isoish;
153
+ return `${isoish}Z`;
154
+ }
155
+ /**
156
+ * Converts an `int8` to the `number` the generated row types declare.
157
+ *
158
+ * Electric's default parser answers with a BigInt, which throws the moment it
159
+ * meets a number in arithmetic — and the row types say `number`, because that is
160
+ * what `JSON.parse` produces on the REST path.
161
+ *
162
+ * Warns rather than throws above the safe-integer range: the REST path loses the
163
+ * same precision silently, so failing here would kill a stream over data the
164
+ * rest of the application accepts. The warning makes the loss visible instead.
165
+ */
166
+ function parseInt8(value) {
167
+ const parsed = Number(value);
168
+ if (!Number.isSafeInteger(parsed)) console.warn(`[rig] int8 value ${value} is outside the safe integer range and lost precision`);
169
+ return parsed;
170
+ }
171
+ //#endregion
172
+ //#region src/shape-url.ts
173
+ /**
174
+ * The address one stream subscribes to.
175
+ *
176
+ * It exists because the sync client resolves nothing: it hands its `url` to
177
+ * `new URL()` with no base. So the relative origin `@rig-ts/client` documents as
178
+ * the ordinary same-origin case — `baseUrl: ""`, resolved against the page —
179
+ * arrives there as `/api/v1/todo/_stream` and throws `Invalid URL`. And because
180
+ * a TypeError is not a `FetchError`, the error handler reads it as a failure
181
+ * worth another go and the stream retries the same unusable address with
182
+ * backoff, silently, rather than reporting anything. Resolving it here is what
183
+ * makes the documented default work for a stream as well as for a REST call,
184
+ * where `fetch` does this much itself.
185
+ *
186
+ * Off a browser there is no page to resolve against, so a relative origin is
187
+ * left as it is. Nothing should be syncing during a server render, and a stream
188
+ * that starts anyway should fail naming the origin it was not given rather than
189
+ * one this invented for it.
190
+ */
191
+ function shapeUrl(origin, path) {
192
+ const url = `${origin}${path}`;
193
+ if (typeof window === "undefined") return url;
194
+ return new URL(url, window.location.href).toString();
195
+ }
196
+ //#endregion
197
+ //#region src/create-collection.ts
198
+ /**
199
+ * Builds a read-only collection over one of a rig application's shape endpoints.
200
+ *
201
+ * **No mutation handlers.** Writes go through the API and the sync service
202
+ * delivers the resulting rows, so there is no txid handshake to perform here. A
203
+ * collection that could be written to would be a second way to change a row, and
204
+ * one that skips every rule the server applies.
205
+ *
206
+ * **No schema.** TanStack DB validates only on optimistic mutations, so a schema
207
+ * here would never run; the row types come from the generated API types, which
208
+ * are the ones the server sends.
209
+ *
210
+ * Sync does not start when this is called. It begins when the first live query
211
+ * subscribes and pauses when the last one unsubscribes, so an instance held
212
+ * across a navigation resumes rather than re-syncing — which is what
213
+ * {@link createCollectionCache} exists to make possible.
214
+ */
215
+ function createRigCollection(args) {
216
+ return createCollection(electricCollectionOptions({
217
+ shapeOptions: {
218
+ url: shapeUrl(args.runtime.origin, args.path),
219
+ params: serializeParams(args.params ?? {}),
220
+ parser: rigParsers,
221
+ fetchClient: rigFetchClient(args.runtime),
222
+ onError: streamErrorHandler(args.runtime)
223
+ },
224
+ getKey: args.getKey
225
+ }));
226
+ }
227
+ //#endregion
228
+ //#region src/collection-cache.ts
229
+ /**
230
+ * Wraps a collection factory so each distinct client and param set maps to one
231
+ * long-lived instance.
232
+ *
233
+ * Collections are stateful and each one opens a stream, so an instance rebuilt
234
+ * per render or per navigation loses everything the library gives a retained
235
+ * one: data held for its collection time, sync that pauses at zero subscribers
236
+ * and resumes on the next one, and resume-from-offset instead of a full
237
+ * re-sync. Returning the same instance is what makes those pay off — and it is
238
+ * what lets a caller invoke a generated factory during render without a
239
+ * load-bearing `useMemo`.
240
+ *
241
+ * Entries remove themselves once their collection reaches `cleaned-up`, so the
242
+ * map is bounded by what is currently live rather than by everything ever asked
243
+ * for. There is no eviction policy to tune.
244
+ *
245
+ * On the server every call builds a fresh instance. The map would otherwise be
246
+ * module-global and leak one request's collections into the next, and nothing
247
+ * server-side should be syncing anyway.
248
+ *
249
+ * Two things the key deliberately leaves out. The **path** is not in it, because
250
+ * each generated factory wraps its own call to this and so has a map of its own
251
+ * — a caller wrapping two different routes in one cache would collide, which is
252
+ * why the generator does not. And the **credential** is not in it: a client that
253
+ * switches to a different tenant on the same origin keeps the collection it had.
254
+ * The rows it holds were scoped to the old session, so switching tenant should
255
+ * discard the collection rather than expect this to notice.
256
+ */
257
+ function createCollectionCache(build) {
258
+ const collections = /* @__PURE__ */ new Map();
259
+ return (runtime, params) => {
260
+ if (typeof window === "undefined") return build(runtime, params);
261
+ const key = `${runtime.origin}|${paramsCacheKey(params)}`;
262
+ const cached = collections.get(key);
263
+ if (cached !== void 0) return cached;
264
+ const collection = build(runtime, params);
265
+ collection.on("status:change", ({ status }) => {
266
+ if (status === "cleaned-up") collections.delete(key);
267
+ });
268
+ collections.set(key, collection);
269
+ return collection;
270
+ };
271
+ }
272
+ //#endregion
273
+ export { createCollectionCache, createRigCollection, paramsCacheKey, rigFetchClient, rigParsers, serializeParams, shapeUrl, streamErrorHandler };
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@rig-ts/electric",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "sideEffects": false,
6
+ "description": "Live-sync collections over a rig application's shape endpoints",
7
+ "license": "MIT",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.mts",
14
+ "default": "./dist/index.mjs"
15
+ }
16
+ },
17
+ "dependencies": {
18
+ "@electric-sql/client": "1.5.24",
19
+ "@tanstack/db": "0.6.17",
20
+ "@tanstack/electric-db-collection": "0.3.15"
21
+ },
22
+ "peerDependencies": {
23
+ "@rig-ts/client": "^0.1.0"
24
+ },
25
+ "devDependencies": {
26
+ "tsdown": "0.21.10",
27
+ "typescript": "5.9.3",
28
+ "@rig-ts/client": "0.1.0"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/simonjanss/rig.git",
36
+ "directory": "ts/packages/electric"
37
+ },
38
+ "scripts": {
39
+ "build": "tsdown",
40
+ "typecheck": "tsc --noEmit"
41
+ }
42
+ }