@dereekb/firebase 13.41.0 → 13.43.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 (49) hide show
  1. package/eslint/index.esm.js +145 -170
  2. package/eslint/package.json +3 -3
  3. package/index.esm.js +7207 -2997
  4. package/package.json +5 -5
  5. package/src/lib/common/firestore/snapshot/snapshot.field.d.ts +4 -0
  6. package/src/lib/common/storage/context.d.ts +9 -1
  7. package/src/lib/common/storage/driver/accessor.d.ts +12 -0
  8. package/src/lib/common/storage/index.d.ts +1 -0
  9. package/src/lib/common/storage/storage.url.d.ts +69 -0
  10. package/src/lib/model/calendar/calendar.action.d.ts +34 -0
  11. package/src/lib/model/calendar/calendar.api.d.ts +147 -0
  12. package/src/lib/model/calendar/calendar.api.error.d.ts +24 -0
  13. package/src/lib/model/calendar/calendar.d.ts +460 -0
  14. package/src/lib/model/calendar/calendar.expand.d.ts +95 -0
  15. package/src/lib/model/calendar/calendar.ics.d.ts +322 -0
  16. package/src/lib/model/calendar/calendar.id.d.ts +110 -0
  17. package/src/lib/model/calendar/calendar.processing.d.ts +77 -0
  18. package/src/lib/model/calendar/calendar.query.d.ts +76 -0
  19. package/src/lib/model/calendar/calendar.schedule.d.ts +82 -0
  20. package/src/lib/model/calendar/calendar.type.d.ts +188 -0
  21. package/src/lib/model/calendar/calendar.util.d.ts +485 -0
  22. package/src/lib/model/calendar/index.d.ts +12 -0
  23. package/src/lib/model/formspace/formspace.access.d.ts +139 -0
  24. package/src/lib/model/formspace/formspace.action.d.ts +34 -0
  25. package/src/lib/model/formspace/formspace.api.d.ts +216 -0
  26. package/src/lib/model/formspace/formspace.api.error.d.ts +71 -0
  27. package/src/lib/model/formspace/formspace.d.ts +390 -0
  28. package/src/lib/model/formspace/formspace.id.d.ts +60 -0
  29. package/src/lib/model/formspace/formspace.permission.d.ts +47 -0
  30. package/src/lib/model/formspace/formspace.processing.d.ts +86 -0
  31. package/src/lib/model/formspace/formspace.query.d.ts +110 -0
  32. package/src/lib/model/formspace/formspace.task.d.ts +75 -0
  33. package/src/lib/model/formspace/formspace.type.d.ts +267 -0
  34. package/src/lib/model/formspace/formspace.upload.d.ts +204 -0
  35. package/src/lib/model/formspace/formspace.util.d.ts +348 -0
  36. package/src/lib/model/formspace/index.d.ts +13 -0
  37. package/src/lib/model/index.d.ts +2 -0
  38. package/src/lib/model/notification/notification.message.d.ts +83 -0
  39. package/src/lib/model/notification/notification.query.d.ts +41 -0
  40. package/src/lib/model/oidcmodel/oidcmodel.query.d.ts +36 -0
  41. package/src/lib/model/storagefile/storagefile.api.d.ts +42 -2
  42. package/src/lib/model/storagefile/storagefile.create.d.ts +14 -3
  43. package/src/lib/model/storagefile/storagefile.file.d.ts +42 -2
  44. package/src/lib/model/storagefile/storagefile.query.d.ts +36 -0
  45. package/src/lib/model/storagefile/storagefile.upload.d.ts +30 -0
  46. package/src/lib/model/system/index.d.ts +1 -0
  47. package/src/lib/model/system/system.scheduler.d.ts +235 -0
  48. package/test/index.esm.js +24 -1
  49. package/test/package.json +6 -6
@@ -2,6 +2,7 @@ import { type Type } from 'arktype';
2
2
  import { type TargetModelParams, type OnCallCreateModelResult, type FirestoreModelKey } from '../../common';
3
3
  import { type ModelFirebaseCrudFunction, type FirebaseFunctionTypeConfigMap, type ModelFirebaseCrudFunctionConfigMap, type ModelFirebaseFunctionMap, type ModelFirebaseCreateFunction } from '../../client';
4
4
  import { type StorageFileSignedDownloadUrl, type StorageFileTypes } from './storagefile';
5
+ import { type StorageFileUploadScope } from './storagefile.upload';
5
6
  import { type StorageFileKey, type StorageFileId, type StorageFilePurpose } from './storagefile.id';
6
7
  import { type StorageBucketId, type StorageMetadata, type StoragePath, type StorageSlashPath } from '../../common/storage';
7
8
  import { type ContentDispositionString, type ContentTypeMimeType, type Maybe, type Milliseconds, type SlashPath, type SlashPathFile, type UnixDateTimeMillisecondsNumber, type UnixDateTimeSecondsNumber } from '@dereekb/util';
@@ -26,6 +27,14 @@ export interface InitializeAllStorageFilesFromUploadsParams {
26
27
  readonly maxFilesToInitialize?: Maybe<number>;
27
28
  readonly folderPath?: Maybe<StorageSlashPath>;
28
29
  readonly overrideUploadsFolderPath?: Maybe<StorageSlashPath>;
30
+ /**
31
+ * Whether to expedite processing of each initialized file that ends up queued for it.
32
+ *
33
+ * The same option {@link InitializeStorageFileFromUploadParams} already offers for a single file. Without
34
+ * it a file initialized by this sweep waits for the next `processAllQueuedStorageFiles` pass, which is a
35
+ * whole scheduling tick of latency for a purpose whose processing the uploader is waiting on.
36
+ */
37
+ readonly expediteProcessing?: Maybe<boolean>;
29
38
  }
30
39
  export declare const initializeAllStorageFilesFromUploadsParamsType: Type<InitializeAllStorageFilesFromUploadsParams>;
31
40
  /**
@@ -51,13 +60,34 @@ export declare const initializeStorageFileFromUploadParamsType: Type<InitializeS
51
60
  /**
52
61
  * Parameters for triggering processing of a specific StorageFile.
53
62
  *
54
- * Supports various modes: immediate processing, retry checking, force restart,
55
- * and reprocessing already-successful files. Validated with {@link processStorageFileParamsType}.
63
+ * Which flag is required depends on the file's current processing state. A `FAILED` file
64
+ * restarts with no flag, while a `SUCCESS` file needs `processAgainIfSuccessful` (or
65
+ * `forceRestartProcessing`) — note that a file whose processor ran to completion is `SUCCESS`
66
+ * even when the outcome was a rejection, so re-validating a rejected file normally needs one of
67
+ * those flags. `ARCHIVED` and `DO_NOT_PROCESS` files cannot be processed at all.
68
+ *
69
+ * Validated with {@link processStorageFileParamsType}.
56
70
  */
57
71
  export interface ProcessStorageFileParams extends TargetModelParams {
72
+ /**
73
+ * Runs the first step of the processing task inline instead of waiting for the scheduled task runner.
74
+ */
58
75
  readonly runImmediately?: Maybe<boolean>;
76
+ /**
77
+ * Checks an in-flight `PROCESSING` task immediately, instead of waiting for it to age past the
78
+ * stuck-check throttle.
79
+ */
59
80
  readonly checkRetryProcessing?: Maybe<boolean>;
81
+ /**
82
+ * Abandons the file's existing processing task and begins a new one, clearing the completed
83
+ * checkpoints so the flow runs again from the start. For a `PROCESSING` file this is only applied
84
+ * once the retry check runs, so pair it with `checkRetryProcessing` to force a restart while the
85
+ * existing task is still within the stuck-check throttle.
86
+ */
60
87
  readonly forceRestartProcessing?: Maybe<boolean>;
88
+ /**
89
+ * Allows processing a file that has already finished processing and is in the `SUCCESS` state.
90
+ */
61
91
  readonly processAgainIfSuccessful?: Maybe<boolean>;
62
92
  }
63
93
  export declare const processStorageFileParamsType: Type<ProcessStorageFileParams>;
@@ -322,7 +352,17 @@ export interface CreateStorageFileSignedUploadUrlParams {
322
352
  * when omitted.
323
353
  */
324
354
  readonly expiresInMs?: Maybe<Milliseconds>;
355
+ /**
356
+ * The model, and optionally the slot within it, this upload belongs to. Required when the resolved
357
+ * policy sets `requiresScopeInput: true` (e.g. a FormSpace upload, which is keyed by space and slot
358
+ * rather than by the uid alone).
359
+ */
360
+ readonly scope?: Maybe<StorageFileUploadScope>;
325
361
  }
362
+ /**
363
+ * Arktype for a {@link StorageFileUploadScope}.
364
+ */
365
+ export declare const storageFileUploadScopeType: Type<StorageFileUploadScope>;
326
366
  export declare const createStorageFileSignedUploadUrlParamsType: Type<CreateStorageFileSignedUploadUrlParams>;
327
367
  /**
328
368
  * Result of creating a signed upload URL.
@@ -5,7 +5,7 @@ import { type FirestoreDocumentAccessor } from '../../common/firestore/accessor/
5
5
  import { type FirebaseStorageAccessorFile } from '../../common/storage/driver/accessor';
6
6
  import { type StoragePathRef, type StoragePath } from '../../common/storage/storage';
7
7
  import { type FirebaseAuthOwnershipKey, type FirebaseAuthUserId } from '../../common/auth/auth';
8
- import { type StorageFilePurposeSubgroup, type StorageFileGroupId, type StorageFileGroupRelatedStorageFilePurpose, type StorageFileMetadata, type StorageFilePurpose } from './storagefile.id';
8
+ import { type StorageFilePurposeSubgroup, type StorageFileGroupId, type StorageFileGroupRelatedStorageFilePurpose, type StorageFileId, type StorageFileMetadata, type StorageFilePurpose } from './storagefile.id';
9
9
  import { type ReadFirestoreModelKeyInput } from '../../common';
10
10
  /**
11
11
  * Input for creating a StorageFile document paired with its storage path.
@@ -13,8 +13,8 @@ import { type ReadFirestoreModelKeyInput } from '../../common';
13
13
  * Provides all the fields needed to create a fully-configured {@link StorageFile} Firestore document,
14
14
  * including ownership, purpose, group membership, and processing state.
15
15
  *
16
- * Either a `file`, `storagePathRef`, or `storagePath` must be provided to identify the storage location.
17
- * Either an `accessor` or `context` must be provided for Firestore document creation.
16
+ * Either a `file`, `storagePathRef`, `storagePath`, or `storagePathFactory` must be provided to identify the
17
+ * storage location. Either an `accessor` or `context` must be provided for Firestore document creation.
18
18
  *
19
19
  * @template M - type of arbitrary metadata stored in the `d` field
20
20
  */
@@ -43,6 +43,17 @@ export interface CreateStorageFileDocumentPairInput<M extends StorageFileMetadat
43
43
  * File to use when creating the StorageFile.
44
44
  */
45
45
  readonly file?: FirebaseStorageAccessorFile;
46
+ /**
47
+ * Builds the storage path from the id of the StorageFile document being created.
48
+ *
49
+ * Use this when the path should be keyed by the StorageFile itself rather than by the model it belongs
50
+ * to. Two StorageFiles then can never resolve to the same path, so replacing one cannot delete the
51
+ * other's content, and the path is not guessable from the owning model's id.
52
+ *
53
+ * Only consulted when no `file`, `storagePathRef`, or `storagePath` is provided, and only for a
54
+ * DIRECTLY_CREATED StorageFile — a FOR_STORAGE_FILE_GROUP file has a deterministic id by design.
55
+ */
56
+ readonly storagePathFactory?: Maybe<(storageFileId: StorageFileId) => StoragePath>;
46
57
  /**
47
58
  * The display name of the StorageFile.
48
59
  *
@@ -1,11 +1,51 @@
1
- import { type Factory, type FactoryWithRequiredInput, type Maybe, type SlashPathDetails } from '@dereekb/util';
2
- import { type StoragePath } from '../../common/storage/storage';
1
+ import { type ContentTypeMimeType, type Factory, type FactoryWithRequiredInput, type Maybe, type SlashPathDetails, type SlashPathFile } from '@dereekb/util';
2
+ import { type StoragePath, type StorageSlashPath } from '../../common/storage/storage';
3
+ import { type StorageFileDisplayName } from './storagefile.id';
3
4
  import { type StorageCustomMetadata } from '../../common/storage/types';
4
5
  import { type FirebaseStorageAccessorFile } from '../../common/storage/driver/accessor';
5
6
  /**
6
7
  * Input for a {@link StoredFileReader}, carrying the storage bucket and path of the file.
7
8
  */
8
9
  export type StoredFileReaderInput = StoragePath;
10
+ /**
11
+ * Input for {@link storageFileDisplayFileName}.
12
+ */
13
+ export interface StorageFileDisplayFileNameInput {
14
+ /**
15
+ * The StorageFile's own `n`, which is UNTYPED by contract (see {@link StorageFileDisplayName}).
16
+ */
17
+ readonly displayName?: Maybe<StorageFileDisplayName>;
18
+ /**
19
+ * The object's path — a StorageFile's `pathString`, or the `name` off its storage metadata.
20
+ */
21
+ readonly pathString?: Maybe<StorageSlashPath>;
22
+ /**
23
+ * The object's content type, used when the path names no extension.
24
+ */
25
+ readonly contentType?: Maybe<ContentTypeMimeType>;
26
+ }
27
+ /**
28
+ * Composes the name a stored file should be presented to a user under.
29
+ *
30
+ * THE one place that answers "what is this file called". A StorageFile's `n` is untyped by contract, so
31
+ * the extension always comes from the object's own path — which is why a purpose whose destination is not
32
+ * name-keyed (a FormSpace file lives at `.../{index}.{ext}`) still downloads and zips under the name its
33
+ * uploader gave it.
34
+ *
35
+ * Falls back to the path's own leaf when there is no display name, so a StorageFile that never had one
36
+ * behaves exactly as it did before.
37
+ *
38
+ * @param input - The display name, the object path, and the content type.
39
+ * @returns The composed file name, or null when the input names neither.
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * storageFileDisplayFileName({ displayName: 'resume', pathString: '/fsp/f1/resume/0.pdf' }); // 'resume.pdf'
44
+ * ```
45
+ *
46
+ * @__NO_SIDE_EFFECTS__
47
+ */
48
+ export declare function storageFileDisplayFileName(input: StorageFileDisplayFileNameInput): Maybe<SlashPathFile>;
9
49
  /**
10
50
  * Factory that creates a {@link StoredFileReader} from a {@link FirebaseStorageAccessorFile}.
11
51
  *
@@ -9,6 +9,11 @@ import { type FirebaseAuthUserId } from '../../common/auth/auth';
9
9
  *
10
10
  * @returns Firestore query constraints for StorageFiles queued for processing.
11
11
  *
12
+ * @dbxModelFirebaseIndex
13
+ * @dbxModelFirebaseIndexModel StorageFile
14
+ * @dbxModelFirebaseIndexScope COLLECTION
15
+ * @dbxModelFirebaseIndexCategory sweep
16
+ *
12
17
  * @example
13
18
  * ```ts
14
19
  * const constraints = storageFilesQueuedForProcessingQuery();
@@ -24,6 +29,11 @@ export declare function storageFilesQueuedForProcessingQuery(): FirestoreQueryCo
24
29
  * @param now - Reference time for comparison; defaults to current time.
25
30
  * @returns Firestore query constraints for StorageFiles whose scheduled delete date has passed.
26
31
  *
32
+ * @dbxModelFirebaseIndex
33
+ * @dbxModelFirebaseIndexModel StorageFile
34
+ * @dbxModelFirebaseIndexScope COLLECTION
35
+ * @dbxModelFirebaseIndexCategory cleanup
36
+ *
27
37
  * @example
28
38
  * ```ts
29
39
  * const constraints = storageFilesQueuedForDeleteQuery();
@@ -48,6 +58,12 @@ export interface StorageFilePurposeAndUserQueryInput {
48
58
  * @param input - The user, purpose, and optional subgroup to filter by.
49
59
  * @returns Firestore query constraints for the given purpose and user.
50
60
  *
61
+ * @dbxModelFirebaseIndex
62
+ * @dbxModelFirebaseIndexModel StorageFile
63
+ * @dbxModelFirebaseIndexScope COLLECTION
64
+ * @dbxModelFirebaseIndexCategory lookup
65
+ * @dbxModelFirebaseIndexSkip true
66
+ *
51
67
  * @example
52
68
  * @example
53
69
  * ```ts
@@ -65,6 +81,11 @@ export declare function storageFilePurposeAndUserQuery(input: StorageFilePurpose
65
81
  *
66
82
  * @returns Firestore query constraints for StorageFiles flagged for group synchronization.
67
83
  *
84
+ * @dbxModelFirebaseIndex
85
+ * @dbxModelFirebaseIndexModel StorageFile
86
+ * @dbxModelFirebaseIndexScope COLLECTION
87
+ * @dbxModelFirebaseIndexCategory sweep
88
+ *
68
89
  * @example
69
90
  * @example
70
91
  * ```ts
@@ -79,6 +100,11 @@ export declare function storageFileFlaggedForSyncWithGroupsQuery(): FirestoreQue
79
100
  *
80
101
  * @returns Firestore query constraints for StorageFileGroups needing initialization.
81
102
  *
103
+ * @dbxModelFirebaseIndex
104
+ * @dbxModelFirebaseIndexModel StorageFileGroup
105
+ * @dbxModelFirebaseIndexScope COLLECTION
106
+ * @dbxModelFirebaseIndexCategory init
107
+ *
82
108
  * @example
83
109
  * @example
84
110
  * ```ts
@@ -91,6 +117,11 @@ export declare function storageFileGroupsFlaggedForNeedsInitializationQuery(): F
91
117
  *
92
118
  * @returns Firestore query constraints for StorageFileGroups flagged for content regeneration.
93
119
  *
120
+ * @dbxModelFirebaseIndex
121
+ * @dbxModelFirebaseIndexModel StorageFileGroup
122
+ * @dbxModelFirebaseIndexScope COLLECTION
123
+ * @dbxModelFirebaseIndexCategory sweep
124
+ *
94
125
  * @example
95
126
  * @example
96
127
  * ```ts
@@ -105,6 +136,11 @@ export declare function storageFileGroupsFlaggedForContentRegenerationQuery(): F
105
136
  *
106
137
  * @returns Firestore query constraints for StorageFileGroups flagged as invalid.
107
138
  *
139
+ * @dbxModelFirebaseIndex
140
+ * @dbxModelFirebaseIndexModel StorageFileGroup
141
+ * @dbxModelFirebaseIndexScope COLLECTION
142
+ * @dbxModelFirebaseIndexCategory cleanup
143
+ *
108
144
  * @example
109
145
  * @example
110
146
  * ```ts
@@ -105,6 +105,28 @@ export type StorageFileInitializeFromUploadResultType = 'success' | 'no_determin
105
105
  export interface StorageFilePurposeUploadPolicyBuildPathInput {
106
106
  readonly uid: FirebaseAuthUserId;
107
107
  readonly filename?: Maybe<SlashPathFile>;
108
+ /**
109
+ * The model the upload is scoped to, for a purpose whose destination is not derivable from the uid alone.
110
+ *
111
+ * Required when the policy sets {@link StorageFilePurposeUploadPolicy.requiresScopeInput}.
112
+ */
113
+ readonly scope?: Maybe<StorageFileUploadScope>;
114
+ }
115
+ /**
116
+ * Names the specific model, and optionally the slot within it, that an upload belongs to.
117
+ *
118
+ * This is what lets ONE purpose serve many destinations: a FormSpace upload is `{ id: formSpaceId,
119
+ * subgroup: slot }` under a single `form_space` purpose, rather than a purpose per form type.
120
+ */
121
+ export interface StorageFileUploadScope {
122
+ /**
123
+ * Id of the model the upload is scoped to.
124
+ */
125
+ readonly id: string;
126
+ /**
127
+ * Slot/subgroup within the scoped model, when the model has more than one.
128
+ */
129
+ readonly subgroup?: Maybe<string>;
108
130
  }
109
131
  /**
110
132
  * Per-purpose constraints for generating short-lived signed upload URLs.
@@ -128,4 +150,12 @@ export interface StorageFilePurposeUploadPolicy {
128
150
  * When false (e.g. avatar), the path is derived solely from the uid.
129
151
  */
130
152
  readonly requiresFilenameInput: boolean;
153
+ /**
154
+ * When true, the caller MUST provide a {@link StorageFileUploadScope} for `buildUploadPath`.
155
+ *
156
+ * Set by a purpose whose destination folder is keyed by a model rather than by the uid alone. The scope
157
+ * is only a PATH input — it is not itself authorization; the purpose's initializer is what loads the
158
+ * scoped model and decides whether this uploader may write into it.
159
+ */
160
+ readonly requiresScopeInput?: boolean;
131
161
  }
@@ -1,2 +1,3 @@
1
1
  export * from './system';
2
2
  export * from './system.action';
3
+ export * from './system.scheduler';
@@ -0,0 +1,235 @@
1
+ /**
2
+ * @module system.scheduler
3
+ *
4
+ * Declares the framework-owned `scheduler` {@link SystemState} type, plus the pure predicates and
5
+ * the moment-bound read object that make up the "has this schedule already run in this hour?" gate.
6
+ *
7
+ * The gate exists so a cron that fires every hour can run a body that should only run every Nth
8
+ * hour, without each individual task having to carry a throttle of its own. Evaluate it ONCE at the
9
+ * top of a schedule function: pass, run the work; fail, return.
10
+ *
11
+ * The stateful half — read, evaluate, and claim the hour against Firestore — lives in
12
+ * `@dereekb/firebase-server/model` as `schedulerSystemStateAccessorFactory()`. Only the shape, the
13
+ * predicates, and {@link schedulerSystemStateRead} live here, so the evaluation can be exercised
14
+ * without an emulator and the converter can be registered from an app's client-shared converter
15
+ * map.
16
+ */
17
+ import { type HourOfDay, type Hours, type Maybe } from '@dereekb/util';
18
+ import { type FirestoreDocument, type FirestoreDocumentAccessor } from '../../common';
19
+ import { type SystemState, type SystemStateDocument, type SystemStateStoredData, type SystemStateStoredDataFieldConverterConfig } from './system';
20
+ /**
21
+ * {@link SystemState} type identifier for the scheduler's run-gate state.
22
+ *
23
+ * Also the document id, per the SystemState singleton convention — so this state lives at
24
+ * `sys/scheduler`.
25
+ */
26
+ export declare const SCHEDULER_SYSTEM_STATE_TYPE = "scheduler";
27
+ /**
28
+ * Scheduler state for gating scheduled work to at most one run per hour window.
29
+ *
30
+ * ONE gate, ONE `lat`. This is deliberately not per-task and not per-table state: it answers
31
+ * "has the scheduler already run its Nth-hour body during this hour?" for the app as a whole. Two
32
+ * callers sharing the document with different `everyNHours` values will have whichever one passes
33
+ * first claim the hour for both.
34
+ *
35
+ * @dbxModelSubObject
36
+ */
37
+ export interface SchedulerSystemData extends SystemStateStoredData {
38
+ /**
39
+ * Last run at. The single gate anchor — the moment the most recent passing check claimed its hour.
40
+ *
41
+ * @dbxModelVariable lastRunAt
42
+ */
43
+ lat?: Maybe<Date>;
44
+ }
45
+ /**
46
+ * Firestore field converter for {@link SchedulerSystemData}.
47
+ *
48
+ * Register it in the app's `SystemStateStoredDataConverterMap` under
49
+ * {@link SCHEDULER_SYSTEM_STATE_TYPE}. This is NOT optional: without it the collection falls back to
50
+ * the pass-through converter, `lat` reads back as a raw Firestore `Timestamp` instead of a `Date`,
51
+ * and {@link hasRunInCurrentHour} can never match — so the gate would open on every single call.
52
+ */
53
+ export declare const schedulerSystemDataConverter: SystemStateStoredDataFieldConverterConfig<SchedulerSystemData>;
54
+ /**
55
+ * Loads the {@link SystemStateDocument} that stores {@link SchedulerSystemData}, using
56
+ * {@link SCHEDULER_SYSTEM_STATE_TYPE} as the document id.
57
+ *
58
+ * @param accessor - The document accessor for the SystemState collection.
59
+ * @returns The SystemState document for the scheduler state.
60
+ *
61
+ * @example
62
+ * ```ts
63
+ * const doc = loadSchedulerSystemState(systemStateCollection.documentAccessor());
64
+ * const data = await doc.snapshotData();
65
+ * ```
66
+ */
67
+ export declare function loadSchedulerSystemState<D extends FirestoreDocument<SystemState<SystemStateStoredData>>>(accessor: FirestoreDocumentAccessor<SystemState<SystemStateStoredData>, D>): SystemStateDocument<SchedulerSystemData>;
68
+ /**
69
+ * Whether the date's hour-of-day is an Nth hour, in the ambient timezone.
70
+ *
71
+ * This is a modulo against the hour-of-day, NOT an interval since some epoch — so at hour 12 it is
72
+ * true for N = 1, 2, 3, 4, 6, and 12, and false for N = 5. Only divisors of 24 divide the day
73
+ * evenly; a non-divisor (5, 7, 9, …) will still fire, just with a short window across midnight.
74
+ *
75
+ * An `everyNHours` of zero or less has no meaningful Nth hour and returns false rather than
76
+ * dividing by zero.
77
+ *
78
+ * @param everyNHours - Run every Nth hour of the day.
79
+ * @param date - Moment to test; defaults to now.
80
+ * @returns True when the date's hour-of-day is an Nth hour.
81
+ *
82
+ * @dbxUtil
83
+ * @dbxUtilCategory date
84
+ * @dbxUtilTags schedule, hour, throttle, cron, gate, modulo
85
+ * @dbxUtilRelated has-run-in-current-hour
86
+ *
87
+ * @example
88
+ * ```ts
89
+ * isNthHourOfDay(3, new Date('2024-01-01T12:30:00')); // true (12 % 3 === 0)
90
+ * isNthHourOfDay(5, new Date('2024-01-01T12:30:00')); // false (12 % 5 === 2)
91
+ * ```
92
+ */
93
+ export declare function isNthHourOfDay(everyNHours: Hours, date?: Maybe<Date>): boolean;
94
+ /**
95
+ * Whether the last run falls inside the same hour as now.
96
+ *
97
+ * This is a same-hour-bucket compare, not an elapsed-time compare: a run at 12:59 and a `now` of
98
+ * 13:01 are two minutes apart but in different hours, so this returns false. That is what makes the
99
+ * gate track the hourly cron rather than drift with it.
100
+ *
101
+ * A null `lastRunAt` means nothing has run, so it returns false.
102
+ *
103
+ * @param lastRunAt - Moment the work last ran.
104
+ * @param now - Moment to compare against; defaults to now.
105
+ * @returns True when both moments fall in the same hour.
106
+ *
107
+ * @dbxUtil
108
+ * @dbxUtilCategory date
109
+ * @dbxUtilTags schedule, hour, throttle, cron, gate, window
110
+ * @dbxUtilRelated is-nth-hour-of-day, round-down-to-hour
111
+ *
112
+ * @example
113
+ * ```ts
114
+ * hasRunInCurrentHour(new Date('2024-01-01T12:05:00'), new Date('2024-01-01T12:55:00')); // true
115
+ * hasRunInCurrentHour(new Date('2024-01-01T12:59:00'), new Date('2024-01-01T13:01:00')); // false
116
+ * ```
117
+ */
118
+ export declare function hasRunInCurrentHour(lastRunAt: Maybe<Date>, now?: Maybe<Date>): boolean;
119
+ /**
120
+ * The zero-based index of the date's hour within the day's every-N-hours schedule, or null when the
121
+ * hour is not an Nth hour.
122
+ *
123
+ * At hour 12 with N=3 the day's matching hours are 0, 3, 6, 9, 12 — so the index is 4. Use it to
124
+ * rotate work across a day's windows ("do the expensive pass only on index 0") without each caller
125
+ * re-deriving the arithmetic from the hour-of-day.
126
+ *
127
+ * Returns null rather than a number for a non-matching hour, so a falsy check cannot confuse
128
+ * "index 0" — the first window of the day — with "no window".
129
+ *
130
+ * @param everyNHours - Run every Nth hour of the day.
131
+ * @param date - Moment to test; defaults to now.
132
+ * @returns The zero-based window index, or null when the hour is not an Nth hour.
133
+ *
134
+ * @dbxUtil
135
+ * @dbxUtilCategory date
136
+ * @dbxUtilTags schedule, hour, throttle, cron, gate, index, window
137
+ * @dbxUtilRelated is-nth-hour-of-day
138
+ *
139
+ * @example
140
+ * ```ts
141
+ * nthHourOfDayIndex(3, new Date('2024-01-01T12:30:00')); // 4 (0, 3, 6, 9, 12)
142
+ * nthHourOfDayIndex(3, new Date('2024-01-01T13:30:00')); // null (13 % 3 !== 0)
143
+ * ```
144
+ */
145
+ export declare function nthHourOfDayIndex(everyNHours: Hours, date?: Maybe<Date>): Maybe<number>;
146
+ /**
147
+ * A read of the scheduler's gate state, evaluated against a single moment.
148
+ *
149
+ * Holds the `now` and the `lastRunAt` it was built from, so every question below is answered against
150
+ * the same moment. Asking about N=2 and then N=3 cannot straddle an hour boundary mid-evaluation,
151
+ * and no question re-reads the document.
152
+ *
153
+ * This is the pure half of the gate. The Firestore-backed half — read, evaluate, and CLAIM the hour
154
+ * in a transaction — is `schedulerSystemStateAccessorFactory()` in `@dereekb/firebase-server/model`.
155
+ */
156
+ export interface SchedulerSystemStateRead {
157
+ /**
158
+ * The moment this read was taken at.
159
+ */
160
+ readonly now: Date;
161
+ /**
162
+ * The hour-of-day of {@link SchedulerSystemStateRead.now}, in the ambient timezone.
163
+ */
164
+ readonly hourOfDay: HourOfDay;
165
+ /**
166
+ * The moment the most recent passing check claimed its hour, or null if the gate has never been
167
+ * claimed.
168
+ */
169
+ readonly lastRunAt: Maybe<Date>;
170
+ /**
171
+ * Whether {@link SchedulerSystemStateRead.lastRunAt} falls inside the same hour as `now` — that is,
172
+ * whether the gate has already been claimed during this hour.
173
+ */
174
+ readonly hasRunInCurrentHour: boolean;
175
+ /**
176
+ * Whether the gate is open for the given interval: `now` is an Nth hour of the day AND the gate
177
+ * has not already been claimed during this hour.
178
+ *
179
+ * This is a pure evaluation — it does NOT claim the hour. Use
180
+ * `SchedulerSystemStateAccessor.checkAndClaim()` to actually gate work.
181
+ *
182
+ * @param everyNHours - Run every Nth hour of the day.
183
+ * @returns True when the gate is open.
184
+ */
185
+ isOpen(everyNHours: Hours): boolean;
186
+ /**
187
+ * {@link isNthHourOfDay} bound to this read's `now`.
188
+ *
189
+ * Unlike {@link SchedulerSystemStateRead.isOpen} this ignores `lastRunAt` entirely, which is what
190
+ * makes it the right sub-gate for work running INSIDE an already-claimed hour: the hour is spent
191
+ * either way, so each task only needs to ask whether this is its hour.
192
+ *
193
+ * @param everyNHours - Run every Nth hour of the day.
194
+ * @returns True when this read's hour-of-day is an Nth hour.
195
+ */
196
+ isNthHourOfDay(everyNHours: Hours): boolean;
197
+ /**
198
+ * {@link nthHourOfDayIndex} bound to this read's `now`.
199
+ *
200
+ * @param everyNHours - Run every Nth hour of the day.
201
+ * @returns The zero-based window index, or null when this read's hour is not an Nth hour.
202
+ */
203
+ nthHourOfDayIndex(everyNHours: Hours): Maybe<number>;
204
+ }
205
+ /**
206
+ * Configuration for {@link schedulerSystemStateRead}.
207
+ */
208
+ export interface SchedulerSystemStateReadConfig {
209
+ /**
210
+ * The moment to evaluate the gate against. Defaults to the current time.
211
+ */
212
+ readonly now?: Maybe<Date>;
213
+ /**
214
+ * The `lat` read off the scheduler document, or null/undefined when it has never been claimed.
215
+ */
216
+ readonly lastRunAt: Maybe<Date>;
217
+ }
218
+ /**
219
+ * Creates a {@link SchedulerSystemStateRead} from a moment and a last-run.
220
+ *
221
+ * Every predicate on the result closes over the single `now` resolved here, so a caller can ask
222
+ * about any number of intervals off one read without drifting across an hour boundary.
223
+ *
224
+ * @param config - The moment and the document's last-run.
225
+ * @returns The evaluated read.
226
+ *
227
+ * @example
228
+ * ```ts
229
+ * const read = schedulerSystemStateRead({ now, lastRunAt });
230
+ *
231
+ * read.isOpen(3); // should the every-3-hours body run?
232
+ * read.isNthHourOfDay(6); // is this also a 6th hour, for a sub-gated task?
233
+ * ```
234
+ */
235
+ export declare function schedulerSystemStateRead(config: SchedulerSystemStateReadConfig): SchedulerSystemStateRead;
package/test/index.esm.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { performAsyncTasks, cachedGetter, bitwiseObjectDencoder, modelFieldConversions, compareStrings, waitForMs, isEvenNumber, unique, arrayFactory, mapGetter, randomNumberFactory, randomFromArrayFactory, idBatchFactory, arrayContainsDuplicateValue, useCallback, readableStreamToBuffer, SLASH_PATH_SEPARATOR } from '@dereekb/util';
2
2
  import { initializeTestEnvironment } from '@firebase/rules-unit-testing';
3
3
  export { assertFails, assertSucceeds } from '@firebase/rules-unit-testing';
4
- import { firebaseStorageContextFactory, firestoreContextFactory, firebaseFirestoreClientDrivers, firebaseStorageClientDrivers, firestoreModelIdentity, snapshotConverterFunctions, firestoreBoolean, optionalFirestoreNumber, optionalFirestoreDate, optionalFirestoreArray, optionalFirestoreString, firestoreDate, firestoreBitwiseObjectMap, firestoreUniqueStringArray, firestoreNumber, firestoreString, firestoreUID, copyUserRelatedDataAccessorFactoryFunction, firestoreSubObject, defaultPagedItemPageDataConverter, AbstractFirestoreDocument, AbstractFirestoreDocumentWithParent, firebaseModelServiceFactory, grantFullAccessIfAdmin, firebaseModelsService, systemStateFirestoreCollection, allChildDocumentsUnderParent, where, makeDocuments, getDocumentSnapshotPairs, useDocumentSnapshot, useDocumentSnapshotData, firestoreIdBatchVerifierFactory, limit, orderBy, limitToLast, whereDocumentId, loadAllFirestoreDocumentSnapshotPairs, loadAllFirestoreDocumentSnapshot, iterateFirestoreDocumentSnapshotPairs, iterateFirestoreDocumentSnapshots, iterateFirestoreDocumentSnapshotPairBatches, iterateFirestoreDocumentSnapshotBatches, whereStringHasRootIdentityModelKey, whereStringValueHasPrefix, whereDateIsAfterWithSort, whereDateIsBeforeWithSort, whereDateIsOnOrAfterWithSort, whereDateIsOnOrBeforeWithSort, whereDateIsInRange, whereDateIsBetween, startAt, orderByDocumentId, startAtValue, startAfter, endAt, endAtValue, endBefore, streamFromOnSnapshot, latestSnapshotsFromDocuments, newDocuments, loadDocumentsForDocumentReferences, loadDocumentsForDocumentReferencesFromValues, loadDocumentsForKeys, loadDocumentsForKeysFromValues, loadDocumentsForIds, loadDocumentsForIdsFromValues, firestoreDocumentLoader, setIdAndKeyFromKeyIdRefOnDocumentData, firestoreModelIdFromDocument, firestoreModelIdsFromDocuments, firestoreModelKeyFromDocument, firestoreModelKeysFromDocuments, documentReferenceFromDocument, documentReferencesFromDocuments, limitedFirestoreDocumentAccessorSnapshotCache, mapLatestSnapshotsFromDocuments, streamDocumentSnapshotsData, dataFromDocumentSnapshots, streamDocumentSnapshotDataPairs, streamDocumentSnapshotDataPairsWithData, getDocumentSnapshots, getDocumentSnapshotPair, getDocumentSnapshotDataPair, getDocumentSnapshotDataPairs, getDocumentSnapshotDataPairsWithData, getDocumentSnapshotDataTuples, getDocumentSnapshotData, getDocumentSnapshotsData, getDataFromDocumentSnapshots, loadDocumentsForSnapshots, firestoreDocumentSnapshotPairsLoader, documentData, documentDataFunction, documentDataWithIdAndKey, setIdAndKeyFromSnapshotOnDocumentData, firebaseQuerySnapshotAccumulator, firebaseQueryItemAccumulator, uploadFileWithStream, iterateStorageListFilesByEachFile } from '@dereekb/firebase';
4
+ import { firebaseStorageContextFactory, firestoreContextFactory, firebaseFirestoreClientDrivers, firebaseStorageClientDrivers, firestoreModelIdentity, snapshotConverterFunctions, firestoreBoolean, optionalFirestoreNumber, optionalFirestoreDate, optionalFirestoreArray, optionalFirestoreString, firestoreDate, firestoreBitwiseObjectMap, firestoreUniqueStringArray, firestoreNumber, firestoreString, firestoreUID, copyUserRelatedDataAccessorFactoryFunction, firestoreSubObject, defaultPagedItemPageDataConverter, AbstractFirestoreDocument, AbstractFirestoreDocumentWithParent, firebaseModelServiceFactory, grantFullAccessIfAdmin, firebaseModelsService, systemStateFirestoreCollection, allChildDocumentsUnderParent, where, makeDocuments, getDocumentSnapshotPairs, useDocumentSnapshot, useDocumentSnapshotData, firestoreIdBatchVerifierFactory, limit, orderBy, limitToLast, whereDocumentId, loadAllFirestoreDocumentSnapshotPairs, loadAllFirestoreDocumentSnapshot, iterateFirestoreDocumentSnapshotPairs, iterateFirestoreDocumentSnapshots, iterateFirestoreDocumentSnapshotPairBatches, iterateFirestoreDocumentSnapshotBatches, whereStringHasRootIdentityModelKey, whereStringValueHasPrefix, whereDateIsAfterWithSort, whereDateIsBeforeWithSort, whereDateIsOnOrAfterWithSort, whereDateIsOnOrBeforeWithSort, whereDateIsInRange, whereDateIsBetween, startAt, orderByDocumentId, startAtValue, startAfter, endAt, endAtValue, endBefore, streamFromOnSnapshot, latestSnapshotsFromDocuments, newDocuments, loadDocumentsForDocumentReferences, loadDocumentsForDocumentReferencesFromValues, loadDocumentsForKeys, loadDocumentsForKeysFromValues, loadDocumentsForIds, loadDocumentsForIdsFromValues, firestoreDocumentLoader, setIdAndKeyFromKeyIdRefOnDocumentData, firestoreModelIdFromDocument, firestoreModelIdsFromDocuments, firestoreModelKeyFromDocument, firestoreModelKeysFromDocuments, documentReferenceFromDocument, documentReferencesFromDocuments, limitedFirestoreDocumentAccessorSnapshotCache, mapLatestSnapshotsFromDocuments, streamDocumentSnapshotsData, dataFromDocumentSnapshots, streamDocumentSnapshotDataPairs, streamDocumentSnapshotDataPairsWithData, getDocumentSnapshots, getDocumentSnapshotPair, getDocumentSnapshotDataPair, getDocumentSnapshotDataPairs, getDocumentSnapshotDataPairsWithData, getDocumentSnapshotDataTuples, getDocumentSnapshotData, getDocumentSnapshotsData, getDataFromDocumentSnapshots, loadDocumentsForSnapshots, firestoreDocumentSnapshotPairsLoader, documentData, documentDataFunction, documentDataWithIdAndKey, setIdAndKeyFromSnapshotOnDocumentData, firebaseQuerySnapshotAccumulator, firebaseQueryItemAccumulator, storagePublicDownloadUrl, uploadFileWithStream, iterateStorageListFilesByEachFile } from '@dereekb/firebase';
5
5
  import { setLogLevel } from 'firebase/firestore';
6
6
  import { AbstractTestContextFixture, instanceWrapTestContextFactory, AbstractWrappedFixtureWithInstance, itShouldFail, callbackTest, expectFail } from '@dereekb/util/test';
7
7
  import { existsSync, readFileSync, createReadStream } from 'node:fs';
@@ -13221,6 +13221,29 @@ function _ts_generator(thisArg, body) {
13221
13221
  })();
13222
13222
  });
13223
13223
  });
13224
+ describe('getPublicUrl()', function() {
13225
+ it('should be reproducible by storagePublicDownloadUrl() from the bucket and path alone.', function() {
13226
+ // The point of this case: the CLIENT derives a public url with storagePublicDownloadUrl() rather
13227
+ // than calling anything, and that derivation is only useful if it matches what the SERVER actually
13228
+ // produces. Only the server driver implements getPublicUrl(), so this pins the shared function
13229
+ // against the real `@google-cloud/storage` output instead of against a reading of its source.
13230
+ if (existsFile.getPublicUrl) {
13231
+ var publicUrl = existsFile.getPublicUrl();
13232
+ var _existsFile_storagePath = existsFile.storagePath, bucketId = _existsFile_storagePath.bucketId, pathString = _existsFile_storagePath.pathString;
13233
+ // split off the origin the server resolved (localhost in the emulator, GCS in production) and
13234
+ // hand it back, so what is under test is the bucket + path half of the url
13235
+ var apiEndpoint = publicUrl.slice(0, publicUrl.indexOf("/".concat(bucketId, "/")));
13236
+ expect(apiEndpoint).toBeTruthy();
13237
+ expect(storagePublicDownloadUrl({
13238
+ apiEndpoint: apiEndpoint,
13239
+ storagePath: {
13240
+ bucketId: bucketId,
13241
+ pathString: pathString
13242
+ }
13243
+ })).toBe(publicUrl);
13244
+ }
13245
+ });
13246
+ });
13224
13247
  // Cannot be tested, will throw "Could not load the default credentials. Browse to https://cloud.google.com/docs/authentication/getting-started for more information."
13225
13248
  describe('getSignedUrl()', function() {
13226
13249
  it('should return the signed read url.', function() {
package/test/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@dereekb/firebase/test",
3
- "version": "13.41.0",
3
+ "version": "13.43.0",
4
4
  "type": "module",
5
5
  "peerDependencies": {
6
- "@dereekb/date": "13.41.0",
7
- "@dereekb/firebase": "13.41.0",
8
- "@dereekb/model": "13.41.0",
9
- "@dereekb/rxjs": "13.41.0",
10
- "@dereekb/util": "13.41.0",
6
+ "@dereekb/date": "13.43.0",
7
+ "@dereekb/firebase": "13.43.0",
8
+ "@dereekb/model": "13.43.0",
9
+ "@dereekb/rxjs": "13.43.0",
10
+ "@dereekb/util": "13.43.0",
11
11
  "@firebase/rules-unit-testing": "5.0.0",
12
12
  "date-fns": "^4.1.0",
13
13
  "firebase": "^12.12.1",