@visiolab/effect-firebase 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 (140) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +81 -2
  3. package/dist/_virtual/_rolldown/runtime.mjs +12 -0
  4. package/dist/app/App.d.mts +27 -0
  5. package/dist/app/App.mjs +27 -0
  6. package/dist/app/index.d.mts +2 -0
  7. package/dist/app/index.mjs +1 -0
  8. package/dist/firestore/Collection.d.mts +423 -0
  9. package/dist/firestore/Collection.mjs +133 -0
  10. package/dist/firestore/CollectionGroup.d.mts +164 -0
  11. package/dist/firestore/CollectionGroup.mjs +84 -0
  12. package/dist/firestore/DocumentPath.d.mts +95 -0
  13. package/dist/firestore/DocumentPath.mjs +71 -0
  14. package/dist/firestore/DocumentSnapshot.d.mts +239 -0
  15. package/dist/firestore/DocumentSnapshot.mjs +134 -0
  16. package/dist/firestore/FieldValue.d.mts +332 -0
  17. package/dist/firestore/FieldValue.mjs +256 -0
  18. package/dist/firestore/Firestore.d.mts +1165 -0
  19. package/dist/firestore/Firestore.mjs +274 -0
  20. package/dist/firestore/FirestoreModel.d.mts +1232 -0
  21. package/dist/firestore/FirestoreModel.mjs +586 -0
  22. package/dist/firestore/FirestoreSchema.d.mts +567 -0
  23. package/dist/firestore/FirestoreSchema.mjs +419 -0
  24. package/dist/firestore/FirestoreValue.d.mts +145 -0
  25. package/dist/firestore/FirestoreValue.mjs +87 -0
  26. package/dist/firestore/GeoPoint.d.mts +90 -0
  27. package/dist/firestore/GeoPoint.mjs +75 -0
  28. package/dist/firestore/Patch.d.mts +73 -0
  29. package/dist/firestore/Patch.mjs +57 -0
  30. package/dist/firestore/QuerySnapshot.d.mts +107 -0
  31. package/dist/firestore/QuerySnapshot.mjs +43 -0
  32. package/dist/firestore/Timestamp.d.mts +273 -0
  33. package/dist/firestore/Timestamp.mjs +255 -0
  34. package/dist/firestore/index.d.mts +14 -0
  35. package/dist/firestore/index.mjs +14 -0
  36. package/dist/firestore/internal/collection.mjs +156 -0
  37. package/dist/firestore/internal/collectionGroup.mjs +64 -0
  38. package/dist/firestore/internal/documentPath.d.mts +4 -0
  39. package/dist/firestore/internal/documentPath.mjs +42 -0
  40. package/dist/firestore/internal/documentSnapshot.mjs +63 -0
  41. package/dist/firestore/internal/fieldValue.d.mts +11 -0
  42. package/dist/firestore/internal/fieldValue.mjs +88 -0
  43. package/dist/firestore/internal/firestore.mjs +145 -0
  44. package/dist/firestore/internal/firestoreError.d.mts +29 -0
  45. package/dist/firestore/internal/firestoreError.mjs +30 -0
  46. package/dist/firestore/internal/firestoreModel.mjs +719 -0
  47. package/dist/firestore/internal/firestoreSchema.mjs +123 -0
  48. package/dist/firestore/internal/firestoreValue.mjs +108 -0
  49. package/dist/firestore/internal/firestoreValueWalk.d.mts +58 -0
  50. package/dist/firestore/internal/firestoreValueWalk.mjs +223 -0
  51. package/dist/firestore/internal/geoPoint.d.mts +4 -0
  52. package/dist/firestore/internal/geoPoint.mjs +39 -0
  53. package/dist/firestore/internal/patch.mjs +19 -0
  54. package/dist/firestore/internal/querySnapshot.mjs +24 -0
  55. package/dist/firestore/internal/schemaAST.mjs +35 -0
  56. package/dist/firestore/internal/snapshotExtra.mjs +35 -0
  57. package/dist/firestore/internal/streamFromListener.mjs +12 -0
  58. package/dist/firestore/internal/timestamp.d.mts +4 -0
  59. package/dist/firestore/internal/timestamp.mjs +66 -0
  60. package/dist/firestore/internal/timestampSchema.mjs +31 -0
  61. package/dist/firestore/internal/transactionFromCallback.mjs +66 -0
  62. package/dist/functions/Callable.d.mts +654 -0
  63. package/dist/functions/Callable.mjs +450 -0
  64. package/dist/functions/CallableError.d.mts +297 -0
  65. package/dist/functions/CallableError.mjs +256 -0
  66. package/dist/functions/CallableMiddleware.d.mts +213 -0
  67. package/dist/functions/CallableMiddleware.mjs +52 -0
  68. package/dist/functions/CallableRequest.d.mts +166 -0
  69. package/dist/functions/CallableRequest.mjs +18 -0
  70. package/dist/functions/Caller.d.mts +103 -0
  71. package/dist/functions/Caller.mjs +115 -0
  72. package/dist/functions/DocumentTrigger.d.mts +795 -0
  73. package/dist/functions/DocumentTrigger.mjs +566 -0
  74. package/dist/functions/FirebaseFunction.d.mts +79 -0
  75. package/dist/functions/FirebaseFunction.mjs +4 -0
  76. package/dist/functions/FunctionGroup.d.mts +175 -0
  77. package/dist/functions/FunctionGroup.mjs +64 -0
  78. package/dist/functions/index.d.mts +9 -0
  79. package/dist/functions/index.mjs +9 -0
  80. package/dist/functions/internal/callable.mjs +167 -0
  81. package/dist/functions/internal/callableMiddleware.d.mts +19 -0
  82. package/dist/functions/internal/callableMiddleware.mjs +15 -0
  83. package/dist/functions/internal/callableRequest.mjs +6 -0
  84. package/dist/functions/internal/documentTrigger.d.mts +10 -0
  85. package/dist/functions/internal/documentTrigger.mjs +73 -0
  86. package/dist/functions/internal/firebaseFunction.d.mts +5 -0
  87. package/dist/functions/internal/firebaseFunction.mjs +24 -0
  88. package/dist/functions/internal/functionGroup.mjs +36 -0
  89. package/package.json +66 -3
  90. package/src/app/App.ts +24 -0
  91. package/src/app/index.ts +7 -0
  92. package/src/firestore/Collection.ts +520 -0
  93. package/src/firestore/CollectionGroup.ts +245 -0
  94. package/src/firestore/DocumentPath.ts +114 -0
  95. package/src/firestore/DocumentSnapshot.ts +308 -0
  96. package/src/firestore/FieldValue.ts +375 -0
  97. package/src/firestore/Firestore.ts +1432 -0
  98. package/src/firestore/FirestoreModel.ts +1884 -0
  99. package/src/firestore/FirestoreSchema.ts +643 -0
  100. package/src/firestore/FirestoreValue.ts +207 -0
  101. package/src/firestore/GeoPoint.ts +105 -0
  102. package/src/firestore/Patch.ts +117 -0
  103. package/src/firestore/QuerySnapshot.ts +150 -0
  104. package/src/firestore/Timestamp.ts +302 -0
  105. package/src/firestore/index.ts +67 -0
  106. package/src/firestore/internal/collection.ts +246 -0
  107. package/src/firestore/internal/collectionGroup.ts +81 -0
  108. package/src/firestore/internal/documentPath.ts +53 -0
  109. package/src/firestore/internal/documentSnapshot.ts +117 -0
  110. package/src/firestore/internal/fieldValue.ts +119 -0
  111. package/src/firestore/internal/firestore.ts +322 -0
  112. package/src/firestore/internal/firestoreError.ts +36 -0
  113. package/src/firestore/internal/firestoreModel.ts +1242 -0
  114. package/src/firestore/internal/firestoreSchema.ts +218 -0
  115. package/src/firestore/internal/firestoreValue.ts +202 -0
  116. package/src/firestore/internal/firestoreValueWalk.ts +349 -0
  117. package/src/firestore/internal/geoPoint.ts +59 -0
  118. package/src/firestore/internal/patch.ts +21 -0
  119. package/src/firestore/internal/querySnapshot.ts +39 -0
  120. package/src/firestore/internal/schemaAST.ts +83 -0
  121. package/src/firestore/internal/snapshotExtra.ts +47 -0
  122. package/src/firestore/internal/streamFromListener.ts +27 -0
  123. package/src/firestore/internal/timestamp.ts +119 -0
  124. package/src/firestore/internal/timestampSchema.ts +42 -0
  125. package/src/firestore/internal/transactionFromCallback.ts +106 -0
  126. package/src/functions/Callable.ts +855 -0
  127. package/src/functions/CallableError.ts +348 -0
  128. package/src/functions/CallableMiddleware.ts +279 -0
  129. package/src/functions/CallableRequest.ts +207 -0
  130. package/src/functions/Caller.ts +141 -0
  131. package/src/functions/DocumentTrigger.ts +927 -0
  132. package/src/functions/FirebaseFunction.ts +96 -0
  133. package/src/functions/FunctionGroup.ts +225 -0
  134. package/src/functions/index.ts +62 -0
  135. package/src/functions/internal/callable.ts +272 -0
  136. package/src/functions/internal/callableMiddleware.ts +54 -0
  137. package/src/functions/internal/callableRequest.ts +8 -0
  138. package/src/functions/internal/documentTrigger.ts +143 -0
  139. package/src/functions/internal/firebaseFunction.ts +58 -0
  140. package/src/functions/internal/functionGroup.ts +47 -0
@@ -0,0 +1,349 @@
1
+ /**
2
+ * The one walk over Firestore Values that the guard and both SDK mappings
3
+ * share: the path to each value, the scalars, arrays and maps, and how a
4
+ * value outside the union is reported. What differs per SDK is only how each
5
+ * kind of value in {@link kinds} becomes the SDK's own, and back.
6
+ */
7
+ import { Equal, Result } from "effect";
8
+ import { FirestoreError } from "./firestoreError.ts";
9
+ import type * as FirestoreValue from "../FirestoreValue.ts";
10
+ import * as documentPath from "./documentPath.ts";
11
+ import * as fieldValue from "./fieldValue.ts";
12
+ import * as geoPoint from "./geoPoint.ts";
13
+ import * as timestamp from "./timestamp.ts";
14
+
15
+ /** @internal */
16
+ export type FieldPath = ReadonlyArray<string | number>;
17
+
18
+ /** @internal */
19
+ export const formatFieldPath = (path: FieldPath): string =>
20
+ path.length === 0
21
+ ? "the document"
22
+ : `"${path
23
+ .map((key, i) => (typeof key === "number" ? `[${key}]` : i === 0 ? key : `.${key}`))
24
+ .join("")}"`;
25
+
26
+ /**
27
+ * The kinds of Firestore Value other than scalars, arrays and maps, each with
28
+ * the guard that recognises it. Adding a kind here makes every SDK's
29
+ * {@link Writer} name it too.
30
+ */
31
+ const kinds = {
32
+ bytes: (u: unknown): u is Uint8Array => u instanceof Uint8Array,
33
+ timestamp: timestamp.isTimestamp,
34
+ geoPoint: geoPoint.isGeoPoint,
35
+ documentPath: documentPath.isDocumentPath,
36
+ serverTimestamp: fieldValue.isServerTimestamp,
37
+ increment: fieldValue.isIncrement,
38
+ deleteField: fieldValue.isDeleteField,
39
+ arrayUnion: fieldValue.isArrayUnion,
40
+ arrayRemove: fieldValue.isArrayRemove,
41
+ } as const;
42
+
43
+ type Guarded<F> = F extends ((u: unknown) => u is infer A) ? A : never;
44
+
45
+ /**
46
+ * Each kind of Firestore Value that is not a scalar, array or map, by name.
47
+ *
48
+ * @internal
49
+ */
50
+ export type Kinds = { readonly [K in keyof typeof kinds]: Guarded<(typeof kinds)[K]> };
51
+
52
+ const kindNames = Object.keys(kinds) as ReadonlyArray<keyof Kinds>;
53
+
54
+ const kindOf = (u: object): keyof Kinds | undefined => kindNames.find((kind) => kinds[kind](u));
55
+
56
+ /**
57
+ * Walks the value a kind holds, such as an array union's elements: `key` is
58
+ * where `child` sits within it.
59
+ *
60
+ * @internal
61
+ */
62
+ export type Walk = (child: unknown, key: string | number) => unknown;
63
+
64
+ /**
65
+ * How one SDK writes each kind of Firestore Value that is not a scalar, array
66
+ * or map.
67
+ *
68
+ * @internal
69
+ */
70
+ export type Writer = {
71
+ readonly [K in keyof Kinds]: (value: Kinds[K], walk: Walk) => unknown;
72
+ };
73
+
74
+ /**
75
+ * How one SDK's values read back as Firestore Values. It hands back
76
+ * `undefined` for a value it leaves to the walk - a scalar, array or map -
77
+ * and calls `fail` for one of its own it cannot read.
78
+ *
79
+ * @internal
80
+ */
81
+ export type Reader = (
82
+ value: object,
83
+ context: {
84
+ readonly walk: (child: unknown, key: string | number) => FirestoreValue.FirestoreValue;
85
+ readonly fail: (kind: string) => never;
86
+ },
87
+ ) => FirestoreValue.FirestoreValue | undefined;
88
+
89
+ /** A map: a plain object whose keys are all strings. */
90
+ const isMap = (u: object): u is Record<string, unknown> => {
91
+ const proto = Object.getPrototypeOf(u);
92
+ return (
93
+ (proto === Object.prototype || proto === null) && Object.getOwnPropertySymbols(u).length === 0
94
+ );
95
+ };
96
+
97
+ const describe = (u: object): string => {
98
+ const name = (u.constructor as { readonly name?: string } | undefined)?.name;
99
+ return name === undefined ? "an object with no prototype" : `a ${name}`;
100
+ };
101
+
102
+ /** A value outside the Firestore Value union, at `path`. */
103
+ class Outside {
104
+ readonly kind: string;
105
+ readonly value: unknown;
106
+ readonly path: FieldPath;
107
+ constructor(kind: string, value: unknown, path: FieldPath) {
108
+ this.kind = kind;
109
+ this.value = value;
110
+ this.path = path;
111
+ }
112
+ }
113
+
114
+ const unhandled: unique symbol = Symbol("unhandled");
115
+
116
+ /**
117
+ * Walks `value`, handing every object to `visit` first and then walking
118
+ * arrays and maps itself. Throws {@link Outside} at the first value that is
119
+ * none of these, or that contains itself.
120
+ */
121
+ const walk = (
122
+ value: unknown,
123
+ visit: (
124
+ u: object,
125
+ walk: (child: unknown, key: string | number) => unknown,
126
+ fail: (kind: string) => never,
127
+ ) => unknown,
128
+ ): unknown => {
129
+ const path: Array<string | number> = [];
130
+ const ancestors = new Set<object>();
131
+ const within = (child: unknown, key: string | number): unknown => {
132
+ path.push(key);
133
+ try {
134
+ return go(child);
135
+ } finally {
136
+ path.pop();
137
+ }
138
+ };
139
+ const go = (u: unknown): unknown => {
140
+ switch (typeof u) {
141
+ case "boolean":
142
+ case "number":
143
+ case "string":
144
+ return u;
145
+ case "object":
146
+ break;
147
+ default:
148
+ throw new Outside(`a value of type ${typeof u}`, u, [...path]);
149
+ }
150
+ if (u === null) return u;
151
+ const fail = (kind: string): never => {
152
+ throw new Outside(kind, u, [...path]);
153
+ };
154
+ if (ancestors.has(u)) return fail("a value that contains itself");
155
+ ancestors.add(u);
156
+ try {
157
+ const out = visit(u, within, fail);
158
+ if (out !== unhandled) return out;
159
+ if (Array.isArray(u)) return u.map((e, i) => within(e, i));
160
+ if (isMap(u)) {
161
+ const map: Record<string, unknown> = {};
162
+ for (const key of Object.keys(u)) map[key] = within(u[key], key);
163
+ return map;
164
+ }
165
+ return fail(describe(u));
166
+ } finally {
167
+ ancestors.delete(u);
168
+ }
169
+ };
170
+ return go(value);
171
+ };
172
+
173
+ const write = (value: unknown, writer: Writer): unknown =>
174
+ walk(value, (u, within) => {
175
+ const kind = kindOf(u);
176
+ return kind === undefined ? unhandled : writer[kind](u as never, within);
177
+ });
178
+
179
+ const keep = <A>(value: A): A => value;
180
+
181
+ /** Builds nothing: walking with it only checks a value is a Firestore Value. */
182
+ const checker: Writer = {
183
+ bytes: keep,
184
+ timestamp: keep,
185
+ geoPoint: keep,
186
+ documentPath: keep,
187
+ serverTimestamp: keep,
188
+ increment: keep,
189
+ deleteField: keep,
190
+ arrayUnion: (u, within) => u.elements.forEach(within),
191
+ arrayRemove: (u, within) => u.elements.forEach(within),
192
+ };
193
+
194
+ /** @internal */
195
+ export const isFirestoreValue = (u: unknown): u is FirestoreValue.FirestoreValue => {
196
+ try {
197
+ write(u, checker);
198
+ return true;
199
+ } catch (e) {
200
+ if (e instanceof Outside) return false;
201
+ throw e;
202
+ }
203
+ };
204
+
205
+ /**
206
+ * Whether two Firestore Values are the same, as Firestore would store them:
207
+ * scalars by value, arrays and maps element by element and key by key, bytes
208
+ * byte by byte, and every other kind as that kind tells its values apart.
209
+ *
210
+ * @internal
211
+ */
212
+ export const equals = (self: unknown, that: unknown): boolean => {
213
+ if (Object.is(self, that)) {
214
+ return true;
215
+ }
216
+ if (typeof self !== "object" || typeof that !== "object" || self === null || that === null) {
217
+ return false;
218
+ }
219
+ if (Array.isArray(self) || Array.isArray(that)) {
220
+ return (
221
+ Array.isArray(self) &&
222
+ Array.isArray(that) &&
223
+ self.length === that.length &&
224
+ self.every((element, i) => equals(element, that[i]))
225
+ );
226
+ }
227
+ const kind = kindOf(self);
228
+ if (kind !== kindOf(that)) {
229
+ return false;
230
+ }
231
+ switch (kind) {
232
+ case undefined:
233
+ return (
234
+ isMap(self) &&
235
+ isMap(that) &&
236
+ Object.keys(self).length === Object.keys(that).length &&
237
+ Object.keys(self).every((key) => Object.hasOwn(that, key) && equals(self[key], that[key]))
238
+ );
239
+ case "bytes": {
240
+ const a = self as Uint8Array;
241
+ const b = that as Uint8Array;
242
+ return a.length === b.length && a.every((byte, i) => byte === b[i]);
243
+ }
244
+ case "arrayUnion":
245
+ case "arrayRemove":
246
+ return equals((self as Kinds["arrayUnion"]).elements, (that as Kinds["arrayUnion"]).elements);
247
+ default:
248
+ return Equal.equals(self, that);
249
+ }
250
+ };
251
+
252
+ /**
253
+ * Turns a Firestore Value into an SDK's own value through `writer`. A value
254
+ * outside the union is a defect, since `toCodecFirestore` only ever encodes
255
+ * Firestore Values.
256
+ *
257
+ * @internal
258
+ */
259
+ export const toSdk = (value: FirestoreValue.FirestoreValue, writer: Writer): unknown => {
260
+ try {
261
+ return write(value, writer);
262
+ } catch (e) {
263
+ if (!(e instanceof Outside)) throw e;
264
+ throw new globalThis.Error(
265
+ `Cannot write ${e.kind} at ${formatFieldPath(e.path)}, which is not a Firestore Value`,
266
+ { cause: e.value },
267
+ );
268
+ }
269
+ };
270
+
271
+ /**
272
+ * Turns what an SDK read into a Firestore Value through `reader`, failing on
273
+ * a value outside the union, such as a vector, and naming the field that
274
+ * holds it.
275
+ *
276
+ * @internal
277
+ */
278
+ export const fromSdk = (
279
+ value: unknown,
280
+ reader: Reader,
281
+ ): Result.Result<FirestoreValue.FirestoreValue, FirestoreError> => {
282
+ try {
283
+ return Result.succeed(
284
+ walk(value, (u, within, fail) => {
285
+ const out = reader(u, {
286
+ walk: within as (child: unknown, key: string | number) => FirestoreValue.FirestoreValue,
287
+ fail,
288
+ });
289
+ return out === undefined ? unhandled : out;
290
+ }) as FirestoreValue.FirestoreValue,
291
+ );
292
+ } catch (e) {
293
+ if (!(e instanceof Outside)) throw e;
294
+ return Result.fail(
295
+ new FirestoreError({
296
+ message: `Firestore holds ${e.kind} at ${formatFieldPath(e.path)}, which is not a Firestore Value`,
297
+ code: undefined,
298
+ cause: e.value,
299
+ }),
300
+ );
301
+ }
302
+ };
303
+
304
+ /**
305
+ * Reads what an SDK holds for the document at `path` as Firestore Values
306
+ * through `reader`, keeping `undefined` - a document that is not there - as
307
+ * it is.
308
+ *
309
+ * @internal
310
+ */
311
+ export const readDocument = (
312
+ path: string,
313
+ data: unknown,
314
+ reader: Reader,
315
+ ): Result.Result<FirestoreValue.FirestoreValue | undefined, FirestoreError> =>
316
+ data === undefined
317
+ ? Result.succeed(undefined)
318
+ : Result.mapError(
319
+ fromSdk(data, reader),
320
+ (error) =>
321
+ new FirestoreError({
322
+ message: `Failed to read the document "${path}": ${error.message}`,
323
+ code: undefined,
324
+ cause: error.cause,
325
+ }),
326
+ );
327
+
328
+ /**
329
+ * Reads Firestore Values as they are, walking into the values a kind holds,
330
+ * so that only a value outside the union fails.
331
+ */
332
+ const asIs: Reader = (u, { walk }) => {
333
+ const kind = kindOf(u);
334
+ if (kind === "arrayUnion" || kind === "arrayRemove") {
335
+ (u as Kinds["arrayUnion"]).elements.forEach((element, i) => walk(element, i));
336
+ }
337
+ return kind === undefined ? undefined : (u as FirestoreValue.FirestoreValue);
338
+ };
339
+
340
+ /**
341
+ * Checks that what a backend read for the document at `path` is made of
342
+ * Firestore Values, failing as {@link readDocument} does on one that is not.
343
+ * It hands back `data` itself, so `undefined` - a document that is not there -
344
+ * stays as it is.
345
+ *
346
+ * @internal
347
+ */
348
+ export const checkDocument = <A>(path: string, data: A): Result.Result<A, FirestoreError> =>
349
+ Result.map(readDocument(path, data, asIs), () => data);
@@ -0,0 +1,59 @@
1
+ import { Equal, Hash, Inspectable, Option, Predicate } from "effect";
2
+ import type * as GeoPoint from "../GeoPoint.ts";
3
+
4
+ /** @internal */
5
+ export const TypeId = "~@visiolab/effect-firebase/firestore/GeoPoint";
6
+
7
+ /** Kept apart from the guards below, which narrow a `number` to `never`. */
8
+ const inLatitudeRange = (n: number): boolean => Number.isFinite(n) && n >= -90 && n <= 90;
9
+
10
+ const inLongitudeRange = (n: number): boolean => Number.isFinite(n) && n >= -180 && n <= 180;
11
+
12
+ /** @internal */
13
+ export const isGeoPoint = (u: unknown): u is GeoPoint.GeoPoint =>
14
+ Predicate.hasProperty(u, TypeId) &&
15
+ u[TypeId] === TypeId &&
16
+ Predicate.hasProperty(u, "latitude") &&
17
+ Predicate.hasProperty(u, "longitude") &&
18
+ Predicate.isNumber(u.latitude) &&
19
+ Predicate.isNumber(u.longitude) &&
20
+ inLatitudeRange(u.latitude) &&
21
+ inLongitudeRange(u.longitude);
22
+
23
+ const Proto: Omit<GeoPoint.GeoPoint, "latitude" | "longitude"> = {
24
+ ...Inspectable.BaseProto,
25
+ [TypeId]: TypeId,
26
+ toJSON(this: GeoPoint.GeoPoint) {
27
+ return { _id: "GeoPoint", latitude: this.latitude, longitude: this.longitude };
28
+ },
29
+ [Equal.symbol](this: GeoPoint.GeoPoint, that: unknown): boolean {
30
+ return isGeoPoint(that) && this.latitude === that.latitude && this.longitude === that.longitude;
31
+ },
32
+ [Hash.symbol](this: GeoPoint.GeoPoint): number {
33
+ return Hash.combine(Hash.number(this.latitude), Hash.number(this.longitude));
34
+ },
35
+ };
36
+
37
+ const makeUnchecked = (latitude: number, longitude: number): GeoPoint.GeoPoint =>
38
+ Object.assign(Object.create(Proto) as typeof Proto, { latitude, longitude });
39
+
40
+ /** @internal */
41
+ export const makeUnsafe = (latitude: number, longitude: number): GeoPoint.GeoPoint => {
42
+ if (!inLatitudeRange(latitude)) {
43
+ throw new Error(
44
+ `Invalid Firestore geopoint: latitude must be a number between -90 and 90, got ${latitude}`,
45
+ );
46
+ }
47
+ if (!inLongitudeRange(longitude)) {
48
+ throw new Error(
49
+ `Invalid Firestore geopoint: longitude must be a number between -180 and 180, got ${longitude}`,
50
+ );
51
+ }
52
+ return makeUnchecked(latitude, longitude);
53
+ };
54
+
55
+ /** @internal */
56
+ export const make = (latitude: number, longitude: number): Option.Option<GeoPoint.GeoPoint> =>
57
+ inLatitudeRange(latitude) && inLongitudeRange(longitude)
58
+ ? Option.some(makeUnchecked(latitude, longitude))
59
+ : Option.none();
@@ -0,0 +1,21 @@
1
+ import { Inspectable, Predicate } from "effect";
2
+ import type * as Patch from "../Patch.ts";
3
+
4
+ /** @internal */
5
+ export const ReplaceTypeId = "~@visiolab/effect-firebase/firestore/Patch/Replace";
6
+
7
+ const ReplaceProto: Omit<Patch.Replace<unknown>, "value"> = {
8
+ ...Inspectable.BaseProto,
9
+ [ReplaceTypeId]: ReplaceTypeId,
10
+ toJSON(this: Patch.Replace<unknown>) {
11
+ return { _id: "Replace", value: Inspectable.toJson(this.value) };
12
+ },
13
+ };
14
+
15
+ /** @internal */
16
+ export const isReplace = (u: unknown): u is Patch.Replace<unknown> =>
17
+ Predicate.hasProperty(u, ReplaceTypeId);
18
+
19
+ /** @internal */
20
+ export const replace = <const A>(value: A): Patch.Replace<A> =>
21
+ Object.assign(Object.create(ReplaceProto) as typeof ReplaceProto, { value });
@@ -0,0 +1,39 @@
1
+ import { Inspectable, Predicate } from "effect";
2
+ import type * as Collection from "../Collection.ts";
3
+ import type * as DocumentSnapshot from "../DocumentSnapshot.ts";
4
+ import type * as QuerySnapshot from "../QuerySnapshot.ts";
5
+ import { attach, extraToJson, type WithExtra } from "./snapshotExtra.ts";
6
+
7
+ /** @internal */
8
+ export const TypeId = "~@visiolab/effect-firebase/firestore/QuerySnapshot";
9
+
10
+ /** @internal */
11
+ export const isQuerySnapshot = (u: unknown): u is QuerySnapshot.QuerySnapshot<Collection.Any> =>
12
+ Predicate.hasProperty(u, TypeId);
13
+
14
+ interface QuerySnapshotImpl extends WithExtra {
15
+ readonly docs: ReadonlyArray<DocumentSnapshot.Existing<Collection.Any>>;
16
+ }
17
+
18
+ const QuerySnapshotProto = {
19
+ ...Inspectable.BaseProto,
20
+ [TypeId]: TypeId,
21
+ toJSON(this: QuerySnapshotImpl) {
22
+ return {
23
+ _id: "QuerySnapshot",
24
+ docs: this.docs.map((doc) => doc.toJSON()),
25
+ ...extraToJson(this),
26
+ };
27
+ },
28
+ };
29
+
30
+ /** @internal */
31
+ export const make = <C extends Collection.Any>(
32
+ docs: ReadonlyArray<DocumentSnapshot.Existing<C>>,
33
+ extra?: object,
34
+ ): QuerySnapshot.QuerySnapshot<C> => {
35
+ const self = Object.assign(Object.create(QuerySnapshotProto) as typeof QuerySnapshotProto, {
36
+ docs,
37
+ });
38
+ return attach(self, extra) as unknown as QuerySnapshot.QuerySnapshot<C>;
39
+ };
@@ -0,0 +1,83 @@
1
+ /**
2
+ * The one place this package reaches into Effect's `@internal` `SchemaAST`
3
+ * helpers.
4
+ *
5
+ * Effect has no public way to define a canonical format, so deriving
6
+ * `toCodecFirestore` the way `toCodecJson` is derived needs the same internal
7
+ * helpers Effect uses for it (ADR 0002). They exist at runtime but are stripped
8
+ * from Effect's type declarations, so each is typed here by hand, against the
9
+ * pinned `effect` version. Keeping them together means a future public API for
10
+ * canonical formats is a swap of this module only.
11
+ */
12
+ import { SchemaAST } from "effect";
13
+
14
+ interface Internals {
15
+ readonly replaceEncoding: <A extends SchemaAST.AST>(
16
+ ast: A,
17
+ encoding: SchemaAST.Encoding | undefined,
18
+ ) => A;
19
+ readonly mapLink: (
20
+ link: SchemaAST.Link,
21
+ f: (ast: SchemaAST.AST) => SchemaAST.AST,
22
+ ) => SchemaAST.Link;
23
+ readonly replaceContextLastLink: <A extends SchemaAST.AST>(
24
+ ast: A,
25
+ context: SchemaAST.Context,
26
+ ) => A;
27
+ readonly applyToSelfOrLastLinkEncodingIdempotent: (
28
+ f: (ast: SchemaAST.AST) => SchemaAST.AST,
29
+ ) => (ast: SchemaAST.AST) => SchemaAST.AST;
30
+ readonly parameterFromString: (ast: SchemaAST.AST) => SchemaAST.AST;
31
+ }
32
+
33
+ const internals = SchemaAST as unknown as Internals;
34
+
35
+ /** Replaces the encoding chain of `ast`, leaving `ast` alone when it is the same. */
36
+ export const replaceEncoding = internals.replaceEncoding;
37
+
38
+ /** Maps the target of `link`, leaving `link` alone when the target is unchanged. */
39
+ export const mapLink = internals.mapLink;
40
+
41
+ /** Replaces the context of the last link's target. */
42
+ export const replaceContextLastLink = internals.replaceContextLastLink;
43
+
44
+ /**
45
+ * Applies `f` to the node itself when it has no encoding, or to the target of
46
+ * its last encoding link otherwise, so user transformations run first. The
47
+ * result is memoized per node.
48
+ */
49
+ export const applyToSelfOrLastLinkEncodingIdempotent =
50
+ internals.applyToSelfOrLastLinkEncodingIdempotent;
51
+
52
+ /** How a canonical format reads index-signature and property keys back from strings. */
53
+ export const parameterFromString = internals.parameterFromString;
54
+
55
+ /** The keywords whose canonical JSON form Effect derives itself. */
56
+ type JsonKeyword =
57
+ | SchemaAST.Undefined
58
+ | SchemaAST.Void
59
+ | SchemaAST.Literal
60
+ | SchemaAST.Number
61
+ | SchemaAST.Symbol
62
+ | SchemaAST.UniqueSymbol
63
+ | SchemaAST.BigInt;
64
+
65
+ /** The keyword's own canonical JSON codec, as `toCodecJson` derives it. */
66
+ export const keywordToCodecJson = (ast: JsonKeyword): SchemaAST.AST =>
67
+ (ast as unknown as { toCodecJson(): SchemaAST.AST }).toCodecJson();
68
+
69
+ /** The keyword's own canonical StringTree codec, as `toCodecStringTree` derives it. */
70
+ export const keywordToCodecStringTree = (ast: JsonKeyword): SchemaAST.AST =>
71
+ (ast as unknown as { toCodecStringTree(): SchemaAST.AST }).toCodecStringTree();
72
+
73
+ /** Recurs into the members of a composite node. */
74
+ export const recur = <A extends SchemaAST.AST>(
75
+ ast: A,
76
+ f: (ast: SchemaAST.AST) => SchemaAST.AST,
77
+ onKey?: (ast: SchemaAST.AST) => SchemaAST.AST,
78
+ ): A =>
79
+ (
80
+ ast as unknown as {
81
+ recur(f: (ast: SchemaAST.AST) => SchemaAST.AST, onKey?: unknown): A;
82
+ }
83
+ ).recur(f, onKey);
@@ -0,0 +1,47 @@
1
+ import { Inspectable } from "effect";
2
+
3
+ /**
4
+ * Where a snapshot keeps the extra an SDK built it with, so that it can show
5
+ * that extra when it prints without telling the extra's fields from its own.
6
+ */
7
+ const ExtraKey = "~@visiolab/effect-firebase/firestore/SnapshotExtra";
8
+
9
+ /** @internal */
10
+ export interface WithExtra {
11
+ readonly [ExtraKey]?: object;
12
+ }
13
+
14
+ /**
15
+ * `self` with the fields of `extra` on it, remembered as its extra so that
16
+ * {@link extraToJson} shows them.
17
+ *
18
+ * The extra itself is held on a key that is not enumerable, so that it does
19
+ * not show up twice: its fields are already on the snapshot.
20
+ *
21
+ * @internal
22
+ */
23
+ export const attach = <A extends object>(self: A, extra: object | undefined): A => {
24
+ if (extra === undefined) {
25
+ return self;
26
+ }
27
+ Object.defineProperty(self, ExtraKey, { value: extra });
28
+ return Object.assign(self, extra);
29
+ };
30
+
31
+ /**
32
+ * How the extra `self` was built with prints: each of its fields as it would
33
+ * print on its own, or nothing when it was built without one.
34
+ *
35
+ * @internal
36
+ */
37
+ export const extraToJson = (self: WithExtra): Record<string, unknown> => {
38
+ const extra = self[ExtraKey];
39
+ if (extra === undefined) {
40
+ return {};
41
+ }
42
+ const json: Record<string, unknown> = {};
43
+ for (const [key, value] of Object.entries(extra)) {
44
+ json[key] = Inspectable.toJson(value);
45
+ }
46
+ return json;
47
+ };
@@ -0,0 +1,27 @@
1
+ import { Cause, Effect, Queue, Result, Stream } from "effect";
2
+ import type * as Firestore from "../Firestore.ts";
3
+ import type { FirestoreError } from "./firestoreError.ts";
4
+
5
+ /** @internal */
6
+ export const streamFromListener = <S, A>(
7
+ options: Firestore.StreamFromListenerOptions<S, A>,
8
+ ): Stream.Stream<A, FirestoreError> =>
9
+ Stream.callback<A, FirestoreError>(
10
+ (queue) =>
11
+ Effect.acquireRelease(
12
+ Effect.sync(() =>
13
+ options.subscribe(
14
+ (snapshot) => {
15
+ // Runs inside the SDK's listener, so a snapshot that does not map
16
+ // fails the stream rather than throwing back into the SDK.
17
+ const element = options.map(snapshot);
18
+ if (Result.isSuccess(element)) Queue.offerUnsafe(queue, element.success);
19
+ else Queue.failCauseUnsafe(queue, Cause.fail(element.failure));
20
+ },
21
+ (error) => Queue.failCauseUnsafe(queue, Cause.fail(options.onError(error))),
22
+ ),
23
+ ),
24
+ (unsubscribe) => Effect.sync(unsubscribe),
25
+ ),
26
+ { bufferSize: 1, strategy: "sliding" },
27
+ );