@visiolab/effect-firebase-client 0.0.0-stage → 0.3.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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +70 -2
  3. package/dist/_virtual/_rolldown/runtime.mjs +12 -0
  4. package/dist/app/App.d.mts +117 -0
  5. package/dist/app/App.mjs +149 -0
  6. package/dist/app/App.mjs.map +1 -0
  7. package/dist/app/index.d.mts +2 -0
  8. package/dist/app/index.mjs +1 -0
  9. package/dist/app/internal/app.mjs +20 -0
  10. package/dist/app/internal/app.mjs.map +1 -0
  11. package/dist/firestore/ClientDocumentSnapshot.d.mts +106 -0
  12. package/dist/firestore/ClientDocumentSnapshot.mjs +86 -0
  13. package/dist/firestore/ClientDocumentSnapshot.mjs.map +1 -0
  14. package/dist/firestore/ClientFirestore.d.mts +331 -0
  15. package/dist/firestore/ClientFirestore.mjs +146 -0
  16. package/dist/firestore/ClientFirestore.mjs.map +1 -0
  17. package/dist/firestore/ClientFirestoreValue.d.mts +61 -0
  18. package/dist/firestore/ClientFirestoreValue.mjs +62 -0
  19. package/dist/firestore/ClientFirestoreValue.mjs.map +1 -0
  20. package/dist/firestore/ClientQuerySnapshot.d.mts +52 -0
  21. package/dist/firestore/ClientQuerySnapshot.mjs +58 -0
  22. package/dist/firestore/ClientQuerySnapshot.mjs.map +1 -0
  23. package/dist/firestore/index.d.mts +5 -0
  24. package/dist/firestore/index.mjs +5 -0
  25. package/dist/firestore/internal/firestore.mjs +158 -0
  26. package/dist/firestore/internal/firestore.mjs.map +1 -0
  27. package/dist/firestore/internal/firestoreValue.mjs +43 -0
  28. package/dist/firestore/internal/firestoreValue.mjs.map +1 -0
  29. package/dist/firestore/internal/snapshotMetadata.mjs +15 -0
  30. package/dist/firestore/internal/snapshotMetadata.mjs.map +1 -0
  31. package/dist/functions/CallableClient.d.mts +139 -0
  32. package/dist/functions/CallableClient.mjs +215 -0
  33. package/dist/functions/CallableClient.mjs.map +1 -0
  34. package/dist/functions/ClientFunctions.d.mts +112 -0
  35. package/dist/functions/ClientFunctions.mjs +105 -0
  36. package/dist/functions/ClientFunctions.mjs.map +1 -0
  37. package/dist/functions/index.d.mts +3 -0
  38. package/dist/functions/index.mjs +3 -0
  39. package/package.json +63 -3
  40. package/src/app/App.ts +163 -0
  41. package/src/app/index.ts +7 -0
  42. package/src/app/internal/app.ts +35 -0
  43. package/src/firestore/ClientDocumentSnapshot.ts +156 -0
  44. package/src/firestore/ClientFirestore.ts +410 -0
  45. package/src/firestore/ClientFirestoreValue.ts +94 -0
  46. package/src/firestore/ClientQuerySnapshot.ts +93 -0
  47. package/src/firestore/index.ts +30 -0
  48. package/src/firestore/internal/firestore.ts +264 -0
  49. package/src/firestore/internal/firestoreValue.ts +58 -0
  50. package/src/firestore/internal/snapshotMetadata.ts +14 -0
  51. package/src/functions/CallableClient.ts +296 -0
  52. package/src/functions/ClientFunctions.ts +149 -0
  53. package/src/functions/index.ts +15 -0
@@ -0,0 +1,410 @@
1
+ /**
2
+ * Firestore on the Firebase client SDK, for browsers.
3
+ *
4
+ * {@link layer} provides one service under two keys: the portable
5
+ * {@link Firestore}, which code that runs on either SDK asks for, and
6
+ * {@link ClientFirestore}, which adds what only the client SDK can do: read
7
+ * sources, listen options, and the
8
+ * {@link ClientDocumentSnapshot.SnapshotMetadata} of every read. Both reach the
9
+ * same database.
10
+ *
11
+ * Where it behaves differently from the Admin SDK:
12
+ *
13
+ * - The emulator is a layer option, not an environment variable.
14
+ * - A write only succeeds once the server acknowledges it, so a write made
15
+ * offline waits for the connection. It is applied to the local cache at once
16
+ * and sent when it can be; interrupting the write does not take it back.
17
+ * - A pending server timestamp reads as the local clock's estimate, not `null`,
18
+ * so it decodes as a timestamp before the server has settled it.
19
+ * - Error codes are always named, such as `permission-denied`.
20
+ *
21
+ * @example Reading a document, and where the read came from
22
+ * ```ts
23
+ * import { Effect, Layer, Schema } from "effect";
24
+ * import { Collection, FirestoreModel } from "@visiolab/effect-firebase/firestore";
25
+ * import { App } from "@visiolab/effect-firebase-client/app";
26
+ * import { ClientFirestore } from "@visiolab/effect-firebase-client/firestore";
27
+ *
28
+ * class Post extends FirestoreModel.Class<Post>("Post")({
29
+ * title: Schema.String,
30
+ * }) {}
31
+ *
32
+ * const Posts = Collection.make("posts", Post);
33
+ *
34
+ * const program = Effect.gen(function* () {
35
+ * const firestore = yield* ClientFirestore.ClientFirestore;
36
+ * const snapshot = yield* firestore.getDoc(Posts.doc(Posts.id.make("hello")));
37
+ * yield* Effect.log(snapshot.metadata.fromCache ? "from the cache" : "from the server");
38
+ * });
39
+ *
40
+ * const FirestoreLive = ClientFirestore.layer().pipe(
41
+ * Layer.provide(App.layer({ apiKey: "<api key>", projectId: "my-project", appId: "<app id>" })),
42
+ * );
43
+ *
44
+ * program.pipe(Effect.provide(FirestoreLive));
45
+ * ```
46
+ *
47
+ * @see {@link AdminFirestore} for the same service on the Admin SDK, for servers
48
+ *
49
+ * @stability unstable
50
+ * @since 0.1.0
51
+ */
52
+ import { Context, Effect, Layer } from "effect";
53
+ import { Firestore, type With } from "@visiolab/effect-firebase/firestore/Firestore";
54
+ import type { Firestore as SdkFirestore, FirestoreLocalCache } from "firebase/firestore";
55
+ import type * as App from "../app/App.ts";
56
+ import type * as ClientDocumentSnapshot from "./ClientDocumentSnapshot.ts";
57
+ // Only {@link ClientFirestore}'s documentation links to it.
58
+ // oxlint-disable-next-line no-unused-vars
59
+ import type * as ClientQuerySnapshot from "./ClientQuerySnapshot.ts";
60
+ import * as internalFirestore from "./internal/firestore.ts";
61
+
62
+ const TypeId = internalFirestore.TypeId;
63
+
64
+ /**
65
+ * The portable {@link Firestore}, plus what only the client SDK can do.
66
+ *
67
+ * **When to use**
68
+ *
69
+ * Ask for it where code needs the client SDK: a read from the server or the
70
+ * cache alone, metadata changes on a listen, a snapshot's
71
+ * {@link ClientDocumentSnapshot.SnapshotMetadata}, or the SDK itself through
72
+ * {@link ClientFirestore.instance}. Everywhere else ask for {@link Firestore},
73
+ * so the code also runs on the Admin SDK.
74
+ *
75
+ * **Details**
76
+ *
77
+ * Its reads take {@link ReadOptions} and its listens {@link ListenOptions}.
78
+ * Every document snapshot, each document of a query snapshot, and each query
79
+ * snapshot as a whole carries the `metadata` of its read, as a
80
+ * {@link ClientDocumentSnapshot.ClientDocumentSnapshot} or
81
+ * {@link ClientQuerySnapshot.ClientQuerySnapshot} does.
82
+ *
83
+ * The service its key holds is also a {@link Firestore}, so it can be handed
84
+ * to code that expects one as it is. The type itself only extends
85
+ * {@link Firestore.With}, so providing a {@link Firestore} alone, such as an
86
+ * in-memory fake, does not satisfy code that asks for it. Its
87
+ * transactions are the portable {@link Firestore.Transaction}: the client SDK's
88
+ * transactions read one document at a time.
89
+ *
90
+ * The client SDK cannot abort a request in flight, so interrupting a call does
91
+ * not stop it: the request still runs to completion.
92
+ *
93
+ * @see {@link Firestore.With} for each read and write it offers
94
+ *
95
+ * @stability unstable
96
+ * @category models
97
+ * @since 0.1.0
98
+ */
99
+ export interface ClientFirestore extends With<
100
+ ReadOptions,
101
+ ListenOptions,
102
+ { readonly metadata: ClientDocumentSnapshot.SnapshotMetadata }
103
+ > {
104
+ readonly [TypeId]: typeof TypeId;
105
+
106
+ /**
107
+ * The client SDK's own `Firestore`, for the database the service was opened
108
+ * on.
109
+ *
110
+ * **When to use**
111
+ *
112
+ * Use for what this service does not wrap, such as queries or
113
+ * `disableNetwork` and `enableNetwork`. Prefer the service's own methods
114
+ * where one exists.
115
+ *
116
+ * **Details**
117
+ *
118
+ * Calls through it skip the collection's model, take and return the SDK's
119
+ * own values, and throw the SDK's own errors. To write a model through it,
120
+ * encode with {@link FirestoreValue.toCodecFirestore} and map with
121
+ * {@link ClientFirestoreValue.toClient}.
122
+ *
123
+ * @see {@link ClientFirestoreValue} for mapping between the SDK's values and Firestore Values
124
+ */
125
+ readonly instance: SdkFirestore;
126
+ }
127
+
128
+ /**
129
+ * How `streamDoc` and `streamDocs` on {@link ClientFirestore} listen, named as
130
+ * in the client SDK's own listen options.
131
+ *
132
+ * **When to use**
133
+ *
134
+ * Use to also hear metadata changes, such as a pending write being
135
+ * acknowledged, or to listen to the local cache alone. Left out, a listen
136
+ * behaves as the SDK's does by default: it emits only when the data changes.
137
+ *
138
+ * **Details**
139
+ *
140
+ * Every snapshot a listen emits carries the
141
+ * {@link ClientDocumentSnapshot.SnapshotMetadata} it arrived with. The portable
142
+ * {@link Firestore.With.streamDoc} takes no options, because the Admin SDK's
143
+ * listeners take none.
144
+ *
145
+ * @example Waiting for the server to acknowledge a write
146
+ * ```ts
147
+ * import { Effect, Schema, Stream } from "effect";
148
+ * import { Collection, FirestoreModel } from "@visiolab/effect-firebase/firestore";
149
+ * import { ClientFirestore } from "@visiolab/effect-firebase-client/firestore";
150
+ *
151
+ * class Post extends FirestoreModel.Class<Post>("Post")({
152
+ * title: Schema.String,
153
+ * }) {}
154
+ *
155
+ * const Posts = Collection.make("posts", Post);
156
+ * const hello = Posts.doc(Posts.id.make("hello"));
157
+ *
158
+ * const acknowledged = Effect.gen(function* () {
159
+ * const firestore = yield* ClientFirestore.ClientFirestore;
160
+ * yield* firestore.streamDoc(hello, { includeMetadataChanges: true }).pipe(
161
+ * Stream.filter((snapshot) => !snapshot.metadata.hasPendingWrites),
162
+ * Stream.runHead,
163
+ * );
164
+ * });
165
+ * ```
166
+ *
167
+ * @see {@link ReadOptions} for the options of a one-off read
168
+ *
169
+ * @stability unstable
170
+ * @category models
171
+ * @since 0.1.0
172
+ */
173
+ export interface ListenOptions {
174
+ /**
175
+ * Whether to also emit when only the
176
+ * {@link ClientDocumentSnapshot.SnapshotMetadata} changes, such as when the
177
+ * server acknowledges a pending write or the cache catches up with the
178
+ * server. Without it, a listen only emits when the data changes.
179
+ */
180
+ readonly includeMetadataChanges?: boolean | undefined;
181
+ /**
182
+ * Where to listen. `"default"` listens to the server and the local cache.
183
+ * `"cache"` listens to the local cache alone and never reaches the server, so
184
+ * it only sees what this client has written or already read.
185
+ */
186
+ readonly source?: "default" | "cache" | undefined;
187
+ }
188
+
189
+ /**
190
+ * Service key for {@link ClientFirestore}.
191
+ *
192
+ * @see {@link Firestore} for the portable key, which {@link layer} provides the same service under
193
+ *
194
+ * @stability unstable
195
+ * @category services
196
+ * @since 0.1.0
197
+ */
198
+ export const ClientFirestore = Context.Service<ClientFirestore, ClientFirestore & Firestore>(
199
+ "@visiolab/effect-firebase-client/firestore/ClientFirestore",
200
+ );
201
+
202
+ /**
203
+ * Where a one-off read is served from.
204
+ *
205
+ * - `"default"`: the server when it can be reached, otherwise the local cache.
206
+ * What a read does when no source is given.
207
+ * - `"server"`: the server alone. Fails with the SDK's code when the server
208
+ * cannot be reached, instead of falling back to the cache.
209
+ * - `"cache"`: the local cache alone, without asking the server.
210
+ *
211
+ * The two reads report what the cache lacks differently, as the SDK does. A
212
+ * cached `getDoc` of a document the cache has never seen fails with a
213
+ * {@link Firestore.FirestoreError} whose code is `unavailable`, and one the
214
+ * cache knows was deleted reads as `Missing`. A cached `getDocs` never fails
215
+ * for what the cache lacks: it returns whatever the cache holds, which may be
216
+ * nothing, with `fromCache: true`.
217
+ *
218
+ * @see {@link ReadOptions} for passing it to a read
219
+ *
220
+ * @stability unstable
221
+ * @category models
222
+ * @since 0.1.0
223
+ */
224
+ export type ReadSource = "default" | "server" | "cache";
225
+
226
+ /**
227
+ * How `getDoc` and `getDocs` on {@link ClientFirestore} read a document, a
228
+ * collection, or a Collection Group.
229
+ *
230
+ * **When to use**
231
+ *
232
+ * Use to read from the server alone, such as to confirm a value before acting
233
+ * on it, or from the local cache alone, such as to render at once while
234
+ * offline. Left out, a read behaves as the SDK's does by default.
235
+ *
236
+ * **Details**
237
+ *
238
+ * With `source: "cache"`, a `getDoc` of a document the cache has never seen
239
+ * fails with a {@link Firestore.FirestoreError} whose code is `unavailable`:
240
+ * not being cached is not the same as not existing. One the cache knows was
241
+ * deleted reads as `Missing`. A `getDocs` never fails for what the cache lacks:
242
+ * it returns whatever the cache holds, which may be nothing, with `fromCache:
243
+ * true`. So, unlike `getDoc`, an empty result cannot tell "nothing cached" from
244
+ * "nothing there".
245
+ *
246
+ * The portable {@link Firestore.With.getDoc} takes no options, because the
247
+ * Admin SDK's reads take none.
248
+ *
249
+ * @example Reading from the server alone
250
+ * ```ts
251
+ * import { Effect, Schema } from "effect";
252
+ * import { Collection, FirestoreModel } from "@visiolab/effect-firebase/firestore";
253
+ * import { ClientFirestore } from "@visiolab/effect-firebase-client/firestore";
254
+ *
255
+ * class Post extends FirestoreModel.Class<Post>("Post")({
256
+ * title: Schema.String,
257
+ * }) {}
258
+ *
259
+ * const Posts = Collection.make("posts", Post);
260
+ *
261
+ * const fresh = Effect.gen(function* () {
262
+ * const firestore = yield* ClientFirestore.ClientFirestore;
263
+ * return yield* firestore.getDoc(Posts.doc(Posts.id.make("hello")), { source: "server" });
264
+ * });
265
+ * ```
266
+ *
267
+ * @see {@link ReadSource} for where a read can be served from
268
+ * @see {@link ListenOptions} for the options of a listen
269
+ *
270
+ * @stability unstable
271
+ * @category models
272
+ * @since 0.1.0
273
+ */
274
+ export interface ReadOptions {
275
+ /**
276
+ * Where the read is served from. Left out, it is `"default"`.
277
+ *
278
+ * @see {@link ReadSource} for each source, and what a cached read does with what the cache lacks
279
+ */
280
+ readonly source?: ReadSource | undefined;
281
+ }
282
+
283
+ /**
284
+ * Where the Firestore emulator listens, such as `{ host: "127.0.0.1", port:
285
+ * 8080 }`.
286
+ *
287
+ * @see {@link Options.emulator} for connecting to it
288
+ *
289
+ * @stability unstable
290
+ * @category models
291
+ * @since 0.1.0
292
+ */
293
+ export interface Emulator {
294
+ readonly host: string;
295
+ readonly port: number;
296
+ }
297
+
298
+ /**
299
+ * How {@link layer} opens a database. All of them are fixed once the database
300
+ * is open.
301
+ *
302
+ * @see {@link AdminFirestore.Options} for the Admin SDK's, which read the emulator from the environment
303
+ *
304
+ * @stability unstable
305
+ * @category models
306
+ * @since 0.1.0
307
+ */
308
+ export interface Options {
309
+ /**
310
+ * The database to open, where the project has more than its default one.
311
+ * Leave it out for the default database.
312
+ */
313
+ readonly databaseId?: string | undefined;
314
+ /**
315
+ * The emulator to talk to instead of the project's database.
316
+ *
317
+ * Unlike the Admin SDK, the client SDK takes the emulator per database, not
318
+ * from the environment, and only before the database is first used.
319
+ */
320
+ readonly emulator?: Emulator | undefined;
321
+ /**
322
+ * Where the SDK keeps the documents it has seen and the writes it has yet to
323
+ * send: `memoryLocalCache()`, the SDK's default, or `persistentLocalCache()`,
324
+ * which keeps them in IndexedDB across reloads.
325
+ */
326
+ readonly localCache?: FirestoreLocalCache | undefined;
327
+ }
328
+
329
+ /**
330
+ * Builds {@link ClientFirestore} on the {@link App.App} in context.
331
+ *
332
+ * **When to use**
333
+ *
334
+ * Use to build the service inside an effect of your own. Most programs provide
335
+ * it with {@link layer} instead, which also provides it as {@link Firestore}.
336
+ *
337
+ * **Details**
338
+ *
339
+ * Building the service cannot fail. Opening a database only throws for an app
340
+ * that was already deleted, or a database already opened or used with other
341
+ * settings. Both are wiring mistakes, so they are defects, and a
342
+ * {@link Firestore.FirestoreError} only ever means that a call failed.
343
+ *
344
+ * A write only succeeds once the server acknowledges it, so a write made
345
+ * offline waits for the connection. It is applied to the local cache at once
346
+ * and sent when it can be: interrupting the write, or timing it out, does not
347
+ * take it back.
348
+ *
349
+ * A server timestamp the server has not settled yet reads as the local clock's
350
+ * estimate, so the field decodes as a timestamp.
351
+ *
352
+ * @see {@link layer} for the same as a layer
353
+ *
354
+ * @stability unstable
355
+ * @category constructors
356
+ * @since 0.1.0
357
+ */
358
+ export const make: (
359
+ options?: Options,
360
+ ) => Effect.Effect<ClientFirestore & Firestore, never, App.App> = Effect.fn("ClientFirestore.make")(
361
+ function* (options?: Options) {
362
+ const firestore = yield* internalFirestore.openDatabase(options);
363
+ return internalFirestore.fromDatabase(firestore);
364
+ },
365
+ );
366
+
367
+ /** The one service, under both its own tag and the portable one. */
368
+ const both = (service: ClientFirestore & Firestore) =>
369
+ Context.make(ClientFirestore, service).pipe(Context.add(Firestore, service));
370
+
371
+ /**
372
+ * A layer that provides {@link ClientFirestore} for the {@link App.App} in
373
+ * context, and the same service as the portable {@link Firestore}.
374
+ *
375
+ * **When to use**
376
+ *
377
+ * Use in a browser, or anywhere the client SDK runs. On a server, use
378
+ * {@link AdminFirestore.layer} instead.
379
+ *
380
+ * **Details**
381
+ *
382
+ * With no options it opens the project's default database with the SDK's
383
+ * defaults.
384
+ *
385
+ * @example Providing Firestore with a persistent cache
386
+ * ```ts
387
+ * import { Layer } from "effect";
388
+ * import { persistentLocalCache } from "firebase/firestore";
389
+ * import { App } from "@visiolab/effect-firebase-client/app";
390
+ * import { ClientFirestore } from "@visiolab/effect-firebase-client/firestore";
391
+ *
392
+ * const FirestoreLive = ClientFirestore.layer({
393
+ * localCache: persistentLocalCache(),
394
+ * emulator: import.meta.env.DEV ? { host: "127.0.0.1", port: 8080 } : undefined,
395
+ * }).pipe(
396
+ * Layer.provide(App.layer({ apiKey: "<api key>", projectId: "my-project", appId: "<app id>" })),
397
+ * );
398
+ * ```
399
+ *
400
+ * @see {@link make} for why building the service cannot fail
401
+ * @see {@link AdminFirestore.layer} for the Admin SDK's layer, for servers
402
+ *
403
+ * @stability unstable
404
+ * @category layers
405
+ * @since 0.1.0
406
+ */
407
+ export const layer = (
408
+ options?: Options,
409
+ ): Layer.Layer<ClientFirestore | Firestore, never, App.App> =>
410
+ Layer.effectContext(Effect.map(make(options), both));
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Maps between Firestore Values, the portable values a model encodes to, and
3
+ * the values the client SDK reads and writes.
4
+ *
5
+ * {@link ClientFirestore} applies it on every read and write. Use it directly
6
+ * to read or write a model through the client SDK itself, such as through
7
+ * {@link ClientFirestore.instance}.
8
+ *
9
+ * @example Writing a model through the client SDK
10
+ * ```ts
11
+ * import { Schema } from "effect";
12
+ * import { FirestoreModel, FirestoreValue } from "@visiolab/effect-firebase/firestore";
13
+ * import { ClientFirestoreValue } from "@visiolab/effect-firebase-client/firestore";
14
+ * import { type DocumentData, doc, getFirestore, setDoc } from "firebase/firestore";
15
+ *
16
+ * class Post extends FirestoreModel.Class<Post>("Post")({
17
+ * title: Schema.String,
18
+ * }) {}
19
+ *
20
+ * const firestore = getFirestore();
21
+ * const encoded = Schema.encodeSync(FirestoreValue.toCodecFirestore(Post.create))({
22
+ * title: "Hello",
23
+ * });
24
+ * await setDoc(
25
+ * doc(firestore, "posts/hello"),
26
+ * ClientFirestoreValue.toClient(encoded, firestore) as DocumentData,
27
+ * );
28
+ * ```
29
+ *
30
+ * @see {@link AdminFirestoreValue} for the Admin SDK's mapping
31
+ *
32
+ * @stability unstable
33
+ * @since 0.3.0
34
+ */
35
+ import type { Result } from "effect";
36
+ import type { FirestoreError } from "@visiolab/effect-firebase/firestore/Firestore";
37
+ import type { FirestoreValue } from "@visiolab/effect-firebase/firestore/FirestoreValue";
38
+ import type { Firestore } from "firebase/firestore";
39
+ import * as internal from "./internal/firestoreValue.ts";
40
+
41
+ /**
42
+ * Turns a Firestore Value into the value the client SDK writes into
43
+ * `firestore`.
44
+ *
45
+ * **When to use**
46
+ *
47
+ * Use to write what a model encodes to through the client SDK itself.
48
+ *
49
+ * **Details**
50
+ *
51
+ * The portable `Timestamp`, `GeoPoint` and bytes become the SDK's `Timestamp`,
52
+ * `GeoPoint` and `Bytes`, and the sentinels its `FieldValue`s. A `DocumentPath`
53
+ * becomes `doc(firestore, path)`, so it points into that database. Map keys are
54
+ * kept as they are, so the dotted field paths of an update stay field paths.
55
+ *
56
+ * It throws for anything that is not a Firestore Value, such as one of the
57
+ * client SDK's own values or a class instance cast past the type. What
58
+ * {@link FirestoreValue.toCodecFirestore} encodes is always a Firestore Value.
59
+ *
60
+ * @see {@link fromClient} for the other direction
61
+ *
62
+ * @stability unstable
63
+ * @category conversions
64
+ * @since 0.3.0
65
+ */
66
+ export const toClient: (value: FirestoreValue, firestore: Firestore) => unknown = internal.toClient;
67
+
68
+ /**
69
+ * Turns a value the client SDK read into a Firestore Value.
70
+ *
71
+ * **When to use**
72
+ *
73
+ * Use to decode what the client SDK itself read through a model, with
74
+ * {@link FirestoreValue.toCodecFirestore}.
75
+ *
76
+ * **Details**
77
+ *
78
+ * The SDK's `Timestamp`, `GeoPoint` and `Bytes` become the portable
79
+ * `Timestamp`, `GeoPoint` and `Uint8Array`. A `DocumentReference` becomes the
80
+ * `DocumentPath` it points at, since Firestore stores nothing else of it;
81
+ * {@link FirestoreSchema.DocumentReference} reads that back into the
82
+ * collection it names.
83
+ *
84
+ * Fails with a {@link Firestore.FirestoreError} naming the field for a value
85
+ * that is not a Firestore Value, such as a vector.
86
+ *
87
+ * @see {@link toClient} for the other direction
88
+ *
89
+ * @stability unstable
90
+ * @category conversions
91
+ * @since 0.3.0
92
+ */
93
+ export const fromClient: (value: unknown) => Result.Result<FirestoreValue, FirestoreError> =
94
+ internal.fromClient;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * What a read of many documents through
3
+ * {@link ClientFirestore.ClientFirestore} returns: a portable
4
+ * {@link QuerySnapshot.QuerySnapshot} that also carries the
5
+ * {@link ClientDocumentSnapshot.SnapshotMetadata} the client SDK reported for
6
+ * the read as a whole, and whose documents carry their own.
7
+ *
8
+ * The snapshot's own metadata says where a read that found nothing came from,
9
+ * since there is no document to ask.
10
+ *
11
+ * @example Telling an empty cache from an empty collection
12
+ * ```ts
13
+ * import { Effect, Schema } from "effect";
14
+ * import { Collection, FirestoreModel } from "@visiolab/effect-firebase/firestore";
15
+ * import { ClientFirestore } from "@visiolab/effect-firebase-client/firestore";
16
+ *
17
+ * class User extends FirestoreModel.Class<User>("User")({
18
+ * name: Schema.String,
19
+ * }) {}
20
+ *
21
+ * const Users = Collection.make("users", User);
22
+ *
23
+ * const program = Effect.gen(function* () {
24
+ * const firestore = yield* ClientFirestore.ClientFirestore;
25
+ * const snapshot = yield* firestore.getDocs(Users.collection());
26
+ * if (snapshot.docs.length === 0 && snapshot.metadata.fromCache) {
27
+ * // Nothing in the local cache, which says nothing of the server.
28
+ * }
29
+ * });
30
+ * ```
31
+ *
32
+ * @see {@link ClientDocumentSnapshot} for what a read of one document returns
33
+ *
34
+ * @stability unstable
35
+ * @since 0.1.0
36
+ */
37
+ import * as QuerySnapshot from "@visiolab/effect-firebase/firestore/QuerySnapshot";
38
+ import type * as Collection from "@visiolab/effect-firebase/firestore/Collection";
39
+ import type * as ClientDocumentSnapshot from "./ClientDocumentSnapshot.ts";
40
+ import * as snapshotMetadata from "./internal/snapshotMetadata.ts";
41
+
42
+ /**
43
+ * A client snapshot of a document that was there when it was read, of
44
+ * whichever of the collections in `C` it is in.
45
+ *
46
+ * @see {@link QuerySnapshot.Existing} for the portable type
47
+ *
48
+ * @stability unstable
49
+ * @category models
50
+ * @since 0.3.0
51
+ */
52
+ export type Existing<C extends Collection.Any> = C extends Collection.Any
53
+ ? ClientDocumentSnapshot.Existing<C>
54
+ : never;
55
+
56
+ /**
57
+ * The documents in `C` a read of many found, with the
58
+ * {@link ClientDocumentSnapshot.SnapshotMetadata} of that read.
59
+ *
60
+ * Each of its `docs` carries its own metadata too, and is of the one
61
+ * collection in `C` it is in.
62
+ *
63
+ * @see {@link QuerySnapshot.QuerySnapshot} for the portable snapshot it extends
64
+ *
65
+ * @stability unstable
66
+ * @category models
67
+ * @since 0.1.0
68
+ */
69
+ export interface ClientQuerySnapshot<C extends Collection.Any> extends QuerySnapshot.QuerySnapshot<
70
+ C,
71
+ { readonly metadata: ClientDocumentSnapshot.SnapshotMetadata }
72
+ > {
73
+ /** Where the read as a whole came from. */
74
+ readonly metadata: ClientDocumentSnapshot.SnapshotMetadata;
75
+ }
76
+
77
+ /**
78
+ * Builds a snapshot of the documents a read of many found, with the
79
+ * `snapshot.metadata` the client SDK reported for the read.
80
+ *
81
+ * **When to use**
82
+ *
83
+ * Use to turn a query snapshot read through the client SDK itself into this
84
+ * library's. Build each document with {@link ClientDocumentSnapshot.existing}.
85
+ *
86
+ * @stability unstable
87
+ * @category constructors
88
+ * @since 0.1.0
89
+ */
90
+ export const make = <C extends Collection.Any>(
91
+ docs: ReadonlyArray<ClientDocumentSnapshot.Existing<C>>,
92
+ metadata: ClientDocumentSnapshot.SnapshotMetadata,
93
+ ): ClientQuerySnapshot<C> => QuerySnapshot.make(docs, snapshotMetadata.extraOf(metadata));
@@ -0,0 +1,30 @@
1
+ /**
2
+ * What a read of one document through Client Firestore returns, with the
3
+ * Snapshot Metadata of the read.
4
+ *
5
+ * @stability unstable
6
+ * @since 0.1.0
7
+ */
8
+ export * as ClientDocumentSnapshot from "./ClientDocumentSnapshot.ts";
9
+ /**
10
+ * Firestore on the Firebase client SDK, for browsers.
11
+ *
12
+ * @stability unstable
13
+ * @since 0.1.0
14
+ */
15
+ export * as ClientFirestore from "./ClientFirestore.ts";
16
+ /**
17
+ * Maps between Firestore Values and the values the client SDK reads and writes.
18
+ *
19
+ * @stability unstable
20
+ * @since 0.3.0
21
+ */
22
+ export * as ClientFirestoreValue from "./ClientFirestoreValue.ts";
23
+ /**
24
+ * What a read of many documents through Client Firestore returns, with the
25
+ * Snapshot Metadata of the read.
26
+ *
27
+ * @stability unstable
28
+ * @since 0.1.0
29
+ */
30
+ export * as ClientQuerySnapshot from "./ClientQuerySnapshot.ts";