@dereekb/firebase 13.43.0 → 14.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/eslint/index.esm.js +209 -302
- package/eslint/package.json +9 -9
- package/index.esm.js +768 -816
- package/package.json +10 -10
- package/src/lib/common/firestore/accessor/document.rxjs.d.ts +0 -4
- package/src/lib/common/firestore/query/accumulator.d.ts +2 -2
- package/src/lib/common/firestore/query/iterator.d.ts +1 -1
- package/src/lib/common/model/function.d.ts +0 -14
- package/src/lib/model/formspace/formspace.api.d.ts +24 -0
- package/src/lib/model/formspace/formspace.api.error.d.ts +25 -0
- package/src/lib/model/formspace/formspace.d.ts +76 -5
- package/src/lib/model/formspace/formspace.task.d.ts +67 -6
- package/src/lib/model/formspace/formspace.type.d.ts +33 -0
- package/src/lib/model/formspace/formspace.util.d.ts +168 -2
- package/src/lib/model/notification/notification.task.d.ts +3 -1
- package/test/index.esm.js +158 -258
- package/test/package.json +8 -8
- package/test/src/lib/common/firebase.instance.d.ts +0 -4
- package/test/src/lib/common/firestore/firestore.instance.d.ts +0 -4
- package/test/src/lib/common/mock/mock.item.collection.fixture.d.ts +0 -7
- package/test/src/lib/common/storage/storage.instance.d.ts +0 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dereekb/firebase",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "14.0.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"exports": {
|
|
@@ -22,22 +22,22 @@
|
|
|
22
22
|
}
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
|
-
"@dereekb/date": "
|
|
26
|
-
"@dereekb/model": "
|
|
27
|
-
"@dereekb/rxjs": "
|
|
28
|
-
"@dereekb/util": "
|
|
29
|
-
"@firebase/rules-unit-testing": "5.0.
|
|
30
|
-
"@marcbachmann/cel-js": "^
|
|
31
|
-
"@typescript-eslint/parser": "8.
|
|
25
|
+
"@dereekb/date": "14.0.1",
|
|
26
|
+
"@dereekb/model": "14.0.1",
|
|
27
|
+
"@dereekb/rxjs": "14.0.1",
|
|
28
|
+
"@dereekb/util": "14.0.1",
|
|
29
|
+
"@firebase/rules-unit-testing": "5.0.2",
|
|
30
|
+
"@marcbachmann/cel-js": "^8.0.0",
|
|
31
|
+
"@typescript-eslint/parser": "8.69.0",
|
|
32
32
|
"arktype": "^2.2.0",
|
|
33
33
|
"date-fns": "^4.1.0",
|
|
34
|
-
"firebase": "^12.
|
|
34
|
+
"firebase": "^12.18.0",
|
|
35
35
|
"make-error": "^1.3.6",
|
|
36
36
|
"rxjs": "^7.8.2",
|
|
37
37
|
"ts-essentials": "^10.2.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"eslint": "10.
|
|
40
|
+
"eslint": "10.9.1"
|
|
41
41
|
},
|
|
42
42
|
"module": "./index.esm.js",
|
|
43
43
|
"main": "./index.esm.js",
|
|
@@ -92,7 +92,3 @@ export declare function streamDocumentSnapshotDataPairs<D extends FirestoreDocum
|
|
|
92
92
|
* @returns Observable emitting snapshot-data pairs for existing documents only, whenever any document changes.
|
|
93
93
|
*/
|
|
94
94
|
export declare function streamDocumentSnapshotDataPairsWithData<D extends FirestoreDocument<any>>(documents: D[]): Observable<FirestoreDocumentSnapshotDataPairWithData<D>[]>;
|
|
95
|
-
/**
|
|
96
|
-
* @deprecated Use {@link streamDocumentSnapshotsData} instead.
|
|
97
|
-
*/
|
|
98
|
-
export declare const latestDataFromDocuments: typeof streamDocumentSnapshotsData;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ItemAccumulatorNextPageUntilResultsCountFunction, type ItemAccumulatorInstance, type ItemAccumulatorMapFunction, type PageItemIteration } from '@dereekb/rxjs';
|
|
1
|
+
import { type ItemAccumulatorNextPageUntilResultsCountFunction, type ItemAccumulatorInstance, type ItemAccumulatorMapFunction, type PageItemIteration, type PageLoadingState } from '@dereekb/rxjs';
|
|
2
2
|
import { type MapFunction } from '@dereekb/util';
|
|
3
3
|
import { type DocumentDataWithIdAndKey, type QueryDocumentSnapshotArray } from '../types';
|
|
4
4
|
import { type FirestoreItemPageIterationInstance } from './iterator';
|
|
@@ -11,7 +11,7 @@ import { type FirestoreItemPageIterationInstance } from './iterator';
|
|
|
11
11
|
* @template O - The output type after mapping the query snapshots
|
|
12
12
|
* @template T - The document data type in the snapshots
|
|
13
13
|
*/
|
|
14
|
-
export type MappedFirebaseQuerySnapshotAccumulator<O, T> = ItemAccumulatorInstance<O, QueryDocumentSnapshotArray<T>, PageItemIteration<QueryDocumentSnapshotArray<T
|
|
14
|
+
export type MappedFirebaseQuerySnapshotAccumulator<O, T> = ItemAccumulatorInstance<O, QueryDocumentSnapshotArray<T>, PageItemIteration<PageLoadingState<QueryDocumentSnapshotArray<T>>>>;
|
|
15
15
|
/**
|
|
16
16
|
* An accumulator that collects Firestore query snapshots without custom mapping.
|
|
17
17
|
*
|
|
@@ -180,7 +180,7 @@ export declare const DEFAULT_FIRESTORE_ITEM_PAGE_ITERATOR_ITEMS_PER_PAGE = 50;
|
|
|
180
180
|
* @__NO_SIDE_EFFECTS__
|
|
181
181
|
*/
|
|
182
182
|
export declare function makeFirestoreItemPageIteratorDelegate<T>(): FirestoreItemPageIteratorDelegate<T>;
|
|
183
|
-
export interface FirestoreItemPageIteration<T> extends MappedPageItemIterationInstance<
|
|
183
|
+
export interface FirestoreItemPageIteration<T> extends MappedPageItemIterationInstance<PageLoadingState<QueryDocumentSnapshotArray<T>>, PageLoadingState<FirestoreItemPageQueryResult<T>>, InternalFirestoreItemPageIterationInstance<T>> {
|
|
184
184
|
/**
|
|
185
185
|
* The underlying iteration instance that works with raw query snapshots.
|
|
186
186
|
*
|
|
@@ -208,17 +208,3 @@ export declare function onCallCreateModelResultWithDocs(result: ArrayOrValue<Doc
|
|
|
208
208
|
* @returns An {@link OnCallCreateModelResult} containing the keys as an array.
|
|
209
209
|
*/
|
|
210
210
|
export declare function onCallCreateModelResult(modelKeys: ArrayOrValue<FirestoreModelKey>): OnCallCreateModelResult;
|
|
211
|
-
/**
|
|
212
|
-
* Creates OnCallTypedModelParams for the input.
|
|
213
|
-
*
|
|
214
|
-
* Convenience function for calling onCallTypedModelParamsFunction and executing it with the input.
|
|
215
|
-
*
|
|
216
|
-
* @param modelTypeInput - The model type string or ref.
|
|
217
|
-
* @param data - The call payload.
|
|
218
|
-
* @param specifier - Optional sub-function specifier.
|
|
219
|
-
* @param call - The CRUD call type.
|
|
220
|
-
* @returns The constructed {@link OnCallTypedModelParams}
|
|
221
|
-
*
|
|
222
|
-
* @deprecated Move towards using onCallTypedModelParamsFunction directly with the call type instead of using this function. Will not be removed in the future.
|
|
223
|
-
*/
|
|
224
|
-
export declare function onCallTypedModelParams<T>(modelTypeInput: FirestoreModelType | FirestoreModelTypeRef, data: T, specifier?: string, call?: OnCallFunctionType): OnCallTypedModelParams<T>;
|
|
@@ -75,6 +75,26 @@ export interface SubmitFormSpaceResult {
|
|
|
75
75
|
*/
|
|
76
76
|
readonly processingTaskCreated: boolean;
|
|
77
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Parameters for reopening a submitted FormSpace back into an editable draft.
|
|
80
|
+
*
|
|
81
|
+
* No options: whether the space may be reopened is the type's policy plus the caller's `reopen` role, and
|
|
82
|
+
* neither is anything a client gets to say. The acting user is taken from the request, never the body,
|
|
83
|
+
* because it is what `rby` records.
|
|
84
|
+
*
|
|
85
|
+
* @dbxModelApiParams
|
|
86
|
+
*/
|
|
87
|
+
export interface ReopenFormSpaceParams extends TargetModelParams {
|
|
88
|
+
}
|
|
89
|
+
export declare const reopenFormSpaceParamsType: Type<ReopenFormSpaceParams>;
|
|
90
|
+
/**
|
|
91
|
+
* Parameters for locking a submitted FormSpace's submission immediately.
|
|
92
|
+
*
|
|
93
|
+
* @dbxModelApiParams
|
|
94
|
+
*/
|
|
95
|
+
export interface LockFormSpaceParams extends TargetModelParams {
|
|
96
|
+
}
|
|
97
|
+
export declare const lockFormSpaceParamsType: Type<LockFormSpaceParams>;
|
|
78
98
|
/**
|
|
79
99
|
* Parameters for removing one uploaded file from a FormSpace slot.
|
|
80
100
|
*
|
|
@@ -175,6 +195,8 @@ export type FormSpaceModelCrudFunctionsConfig = {
|
|
|
175
195
|
update: {
|
|
176
196
|
_: UpdateFormSpaceParams;
|
|
177
197
|
submit: [SubmitFormSpaceParams, SubmitFormSpaceResult];
|
|
198
|
+
reopen: ReopenFormSpaceParams;
|
|
199
|
+
lock: LockFormSpaceParams;
|
|
178
200
|
removeFile: RemoveFormSpaceFileParams;
|
|
179
201
|
};
|
|
180
202
|
delete: {
|
|
@@ -197,6 +219,8 @@ export declare abstract class FormSpaceFunctions implements ModelFirebaseFunctio
|
|
|
197
219
|
updateFormSpace: {
|
|
198
220
|
update: ModelFirebaseCrudFunction<UpdateFormSpaceParams>;
|
|
199
221
|
submit: ModelFirebaseCrudFunction<SubmitFormSpaceParams, SubmitFormSpaceResult>;
|
|
222
|
+
reopen: ModelFirebaseCrudFunction<ReopenFormSpaceParams>;
|
|
223
|
+
lock: ModelFirebaseCrudFunction<LockFormSpaceParams>;
|
|
200
224
|
removeFile: ModelFirebaseCrudFunction<RemoveFormSpaceFileParams>;
|
|
201
225
|
};
|
|
202
226
|
deleteFormSpace: {
|
|
@@ -15,6 +15,31 @@ export declare const FORM_SPACE_TYPE_NOT_REGISTERED_ERROR_CODE = "FORM_SPACE_TYP
|
|
|
15
15
|
* Thrown when a FormSpace is edited, uploaded into, or submitted after it stopped being a draft.
|
|
16
16
|
*/
|
|
17
17
|
export declare const FORM_SPACE_NOT_EDITABLE_ERROR_CODE = "FORM_SPACE_NOT_EDITABLE";
|
|
18
|
+
/**
|
|
19
|
+
* Thrown when a FormSpace's submission is locked before the space was ever submitted.
|
|
20
|
+
*
|
|
21
|
+
* A lock ends a reopen window early, so there has to be a submission for it to be about. A space that was
|
|
22
|
+
* submitted and then reopened DOES qualify — locking a reopened draft is how a reviewer says "this next
|
|
23
|
+
* submission is the last one".
|
|
24
|
+
*/
|
|
25
|
+
export declare const FORM_SPACE_NOT_SUBMITTED_ERROR_CODE = "FORM_SPACE_NOT_SUBMITTED";
|
|
26
|
+
/**
|
|
27
|
+
* Thrown when a submitted FormSpace is reopened after it stopped being reopenable.
|
|
28
|
+
*
|
|
29
|
+
* TERMINAL, unlike {@link FORM_SPACE_PROCESSING_IN_PROGRESS_ERROR_CODE}: the type never allowed reopening,
|
|
30
|
+
* the reopen window has closed, `lat` has passed, or `maxReopens` is spent. Retrying cannot help, so the
|
|
31
|
+
* caller should be told the submission is final rather than asked to wait.
|
|
32
|
+
*/
|
|
33
|
+
export declare const FORM_SPACE_NOT_REOPENABLE_ERROR_CODE = "FORM_SPACE_NOT_REOPENABLE";
|
|
34
|
+
/**
|
|
35
|
+
* Thrown when a FormSpace is reopened while its submission is actively being processed.
|
|
36
|
+
*
|
|
37
|
+
* Transient by nature — the same reopen succeeds once the processor concludes — which is why it is not
|
|
38
|
+
* folded into {@link FORM_SPACE_NOT_REOPENABLE_ERROR_CODE}. Reopening under a running processor would race
|
|
39
|
+
* it: the task's cleanup writes `ps`/`cpat`/`pn` and would land them on a space already handed back as a
|
|
40
|
+
* draft.
|
|
41
|
+
*/
|
|
42
|
+
export declare const FORM_SPACE_PROCESSING_IN_PROGRESS_ERROR_CODE = "FORM_SPACE_PROCESSING_IN_PROGRESS";
|
|
18
43
|
/**
|
|
19
44
|
* Thrown when a FormSpace is submitted while one of its type's required slots is still empty.
|
|
20
45
|
*/
|
|
@@ -31,9 +31,11 @@ export declare const formSpaceIdentity: import("../..").RootFirestoreModelIdenti
|
|
|
31
31
|
/**
|
|
32
32
|
* Lifecycle state of a {@link FormSpace}.
|
|
33
33
|
*
|
|
34
|
-
* Only DRAFT is editable.
|
|
35
|
-
*
|
|
36
|
-
*
|
|
34
|
+
* Only DRAFT is editable. SUBMITTED is awaiting or undergoing processing, and is the one state a space can
|
|
35
|
+
* come BACK from: a type declaring a reopen policy lets a caller holding the `reopen` role return the space
|
|
36
|
+
* to DRAFT until it is fully locked. EXPIRED (retired by the sweep before it was ever submitted) and
|
|
37
|
+
* ARCHIVED (kept for the record after processing concluded) stay terminal — {@link isFormSpaceReopenable}
|
|
38
|
+
* requires SUBMITTED, so neither is reachable by a reopen.
|
|
37
39
|
*/
|
|
38
40
|
export declare enum FormSpaceState {
|
|
39
41
|
DRAFT = 0,
|
|
@@ -295,11 +297,25 @@ export interface FormSpace<T extends FormSpaceData = FormSpaceData> {
|
|
|
295
297
|
*/
|
|
296
298
|
uat: Date;
|
|
297
299
|
/**
|
|
298
|
-
* The date the
|
|
300
|
+
* The date the CURRENT submission was made, if the space is submitted. Its presence IS the lock.
|
|
301
|
+
*
|
|
302
|
+
* Cleared by a reopen, which is what hands the space back as an editable draft, so it always describes
|
|
303
|
+
* the submission in force NOW rather than the history. `fsat` is what remembers the first one.
|
|
299
304
|
*
|
|
300
305
|
* @dbxModelVariable submittedAt
|
|
301
306
|
*/
|
|
302
307
|
sat?: Maybe<Date>;
|
|
308
|
+
/**
|
|
309
|
+
* The date the space was FIRST submitted, if it ever was.
|
|
310
|
+
*
|
|
311
|
+
* Never cleared. Since a reopen clears `sat`, without this the fact that the space was submitted at all —
|
|
312
|
+
* and when — would be destroyed by the first reopen. It is also the anchor
|
|
313
|
+
* {@link resolveFormSpaceLocksAt} measures the type's `reopenableUntil` from, which is what stops a
|
|
314
|
+
* reopen/resubmit round from walking the lock deadline forward.
|
|
315
|
+
*
|
|
316
|
+
* @dbxModelVariable firstSubmittedAt
|
|
317
|
+
*/
|
|
318
|
+
fsat?: Maybe<Date>;
|
|
303
319
|
/**
|
|
304
320
|
* The date processing of the submission concluded, if it has.
|
|
305
321
|
*
|
|
@@ -316,6 +332,54 @@ export interface FormSpace<T extends FormSpaceData = FormSpaceData> {
|
|
|
316
332
|
* @dbxModelVariable expiresAt
|
|
317
333
|
*/
|
|
318
334
|
eat?: Maybe<Date>;
|
|
335
|
+
/**
|
|
336
|
+
* The date reopening stops being possible — the instant the submission becomes FULLY LOCKED.
|
|
337
|
+
*
|
|
338
|
+
* Written once, on the first submit, as `fsat + reopenableUntil`, and never moved afterwards; an explicit
|
|
339
|
+
* lock sets it to that moment instead. Absent means there is no CEILING, not that the space is locked:
|
|
340
|
+
* the type's `reopenableFor` is the master switch, and a type declaring neither is simply never
|
|
341
|
+
* reopenable. The same "an absent field is an absent gate" convention `eat` uses.
|
|
342
|
+
*
|
|
343
|
+
* Stored as an ISO8601 string like every other date here, which means `firestore.rules` CANNOT compare it
|
|
344
|
+
* against `request.time` — rules convert only bool/int/float/null to a string, never a timestamp. A
|
|
345
|
+
* downstream app that needs the lock predicate inside its OWN rules has to denormalize a unix-seconds
|
|
346
|
+
* mirror onto its own model and compare that. Reading this field over a callable, or over the `get` the
|
|
347
|
+
* rules already grant, needs no mirror at all.
|
|
348
|
+
*
|
|
349
|
+
* @dbxModelVariable locksAt
|
|
350
|
+
*/
|
|
351
|
+
lat?: Maybe<Date>;
|
|
352
|
+
/**
|
|
353
|
+
* The user who locked the submission early, when a caller did rather than the deadline passing.
|
|
354
|
+
*
|
|
355
|
+
* @dbxModelVariable lockedBy
|
|
356
|
+
*/
|
|
357
|
+
lby?: Maybe<FirebaseAuthUserId>;
|
|
358
|
+
/**
|
|
359
|
+
* Monotonic count of times this space has been REOPENED after a submission.
|
|
360
|
+
*
|
|
361
|
+
* Doubles as the submission-attempt generation. The submission task is keyed by it, so a resubmit gets a
|
|
362
|
+
* fresh task instead of colliding with the finished one, and a task still carrying a stale count is
|
|
363
|
+
* fenced off rather than clobbering the attempt in force.
|
|
364
|
+
*
|
|
365
|
+
* Monotonic for the same reason `uc` is: it counts rounds that happened, and a counter something can
|
|
366
|
+
* rewind is not a bound. Capped by the type's `maxReopens`.
|
|
367
|
+
*
|
|
368
|
+
* @dbxModelVariable reopenCount
|
|
369
|
+
*/
|
|
370
|
+
rc: number;
|
|
371
|
+
/**
|
|
372
|
+
* The date the space was last reopened, if it ever was.
|
|
373
|
+
*
|
|
374
|
+
* @dbxModelVariable reopenedAt
|
|
375
|
+
*/
|
|
376
|
+
rat?: Maybe<Date>;
|
|
377
|
+
/**
|
|
378
|
+
* The user who last reopened the space.
|
|
379
|
+
*
|
|
380
|
+
* @dbxModelVariable reopenedBy
|
|
381
|
+
*/
|
|
382
|
+
rby?: Maybe<FirebaseAuthUserId>;
|
|
319
383
|
}
|
|
320
384
|
/**
|
|
321
385
|
* Permission roles for FormSpace operations.
|
|
@@ -334,8 +398,15 @@ export interface FormSpace<T extends FormSpaceData = FormSpaceData> {
|
|
|
334
398
|
* context to build a role map from — so `uploadFile` is the DECLARATION of who may contribute, and
|
|
335
399
|
* `FormSpaceUploadAuthorizationDelegate` is where the same app policy is actually applied. Grant them
|
|
336
400
|
* together, to the same people.
|
|
401
|
+
*
|
|
402
|
+
* `reopen` and `lock` are the two halves of undoing that door, and are separate from each other as much as
|
|
403
|
+
* from `submit`. `reopen` returns a submitted space to DRAFT; WHETHER it may be reopened at all is the
|
|
404
|
+
* type's own policy, re-asserted inside the action's transaction, so the role only says who is allowed to
|
|
405
|
+
* ask. `lock` goes the other way and ends the reopen window early — in a two-party flow that is a
|
|
406
|
+
* privilege the party who submitted should not automatically hold over the party reviewing, which is
|
|
407
|
+
* exactly why it is not folded into `reopen`.
|
|
337
408
|
*/
|
|
338
|
-
export type FormSpaceRoles = GrantedReadRole | GrantedUpdateRole | GrantedDeleteRole | 'submit' | 'uploadFile' | 'removeFile';
|
|
409
|
+
export type FormSpaceRoles = GrantedReadRole | GrantedUpdateRole | GrantedDeleteRole | 'submit' | 'uploadFile' | 'removeFile' | 'reopen' | 'lock';
|
|
339
410
|
/**
|
|
340
411
|
* Firestore document wrapper for a {@link FormSpace}.
|
|
341
412
|
*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Maybe } from '@dereekb/util';
|
|
2
2
|
import { type CreateNotificationTaskTemplate } from '../notification/notification.create.task';
|
|
3
3
|
import { type NotificationTaskSubtaskCheckpointString, type NotificationTaskSubtaskData, type NotificationTaskSubtaskMetadata } from '../notification/notification.task.subtask';
|
|
4
|
-
import { type NotificationTaskType } from '../notification/notification.id';
|
|
4
|
+
import { type NotificationTaskType, type NotificationTaskUniqueId } from '../notification/notification.id';
|
|
5
5
|
import { type FormSpaceDocument } from './formspace';
|
|
6
6
|
import { type FormSpaceId, type FormSpaceType } from './formspace.id';
|
|
7
7
|
/**
|
|
@@ -12,6 +12,14 @@ import { type FormSpaceId, type FormSpaceType } from './formspace.id';
|
|
|
12
12
|
* ONE task type for every form type. The task's SUBTASK TARGET is the {@link FormSpaceType}, so a new form
|
|
13
13
|
* type registers a processor rather than a task type — the same specialization
|
|
14
14
|
* `StorageFileProcessingNotificationTask` already does by purpose.
|
|
15
|
+
*
|
|
16
|
+
* ONE task per SUBMISSION ATTEMPT, not per space. A unique task's document id is derived and permanent, and
|
|
17
|
+
* a completed task is only marked done (`d`) — it lingers until the cleanup sweep collects it. So a space
|
|
18
|
+
* that is reopened and resubmitted would re-derive the id of its own finished task, and
|
|
19
|
+
* `createOrRunUniqueNotificationDocument` would find it already there and do nothing at all: the space
|
|
20
|
+
* would sit in QUEUED_FOR_PROCESSING pointing at a dead document forever. Keying the id by the space's
|
|
21
|
+
* reopen count is what keeps each attempt a document of its own, and the first attempt keeps the exact id
|
|
22
|
+
* it has always had.
|
|
15
23
|
*/
|
|
16
24
|
/**
|
|
17
25
|
* NotificationTask type identifier for FormSpace submission processing.
|
|
@@ -43,26 +51,79 @@ export interface FormSpaceSubmissionNotificationTaskData<M extends FormSpaceSubm
|
|
|
43
51
|
* so subsequent runs do not re-read the document only to learn which processor to dispatch to.
|
|
44
52
|
*/
|
|
45
53
|
readonly t?: Maybe<FormSpaceType>;
|
|
54
|
+
/**
|
|
55
|
+
* The space's reopen count when this task was created — the submission ATTEMPT this task belongs to.
|
|
56
|
+
*
|
|
57
|
+
* The handler compares it against the space's current count and terminates when they differ. Without
|
|
58
|
+
* that fence a task left over from a superseded attempt would process the reopened space's new content
|
|
59
|
+
* and its cleanup would write `ps`/`cpat`/`pn` over the attempt actually in force. Absent on a task
|
|
60
|
+
* created before attempt-keying existed, which is read as "do not fence".
|
|
61
|
+
*/
|
|
62
|
+
readonly rc?: Maybe<number>;
|
|
46
63
|
}
|
|
47
64
|
/**
|
|
48
65
|
* Input for {@link formSpaceSubmissionNotificationTaskTemplate}.
|
|
49
66
|
*/
|
|
50
|
-
export interface FormSpaceSubmissionNotificationTaskInput<M extends FormSpaceSubmissionSubtaskMetadata = FormSpaceSubmissionSubtaskMetadata> extends Omit<FormSpaceSubmissionNotificationTaskData<M>, 'formSpace' | 't' | 'sfps'> {
|
|
67
|
+
export interface FormSpaceSubmissionNotificationTaskInput<M extends FormSpaceSubmissionSubtaskMetadata = FormSpaceSubmissionSubtaskMetadata> extends Omit<FormSpaceSubmissionNotificationTaskData<M>, 'formSpace' | 't' | 'sfps' | 'rc'> {
|
|
51
68
|
readonly formSpaceDocument: FormSpaceDocument;
|
|
69
|
+
/**
|
|
70
|
+
* The space's current reopen count — the attempt this task is for. Defaults to 0, the first submission.
|
|
71
|
+
*/
|
|
72
|
+
readonly reopenCount?: Maybe<number>;
|
|
52
73
|
readonly overrideExistingTask?: Maybe<boolean>;
|
|
53
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Input for {@link formSpaceSubmissionNotificationTaskUniqueId}.
|
|
77
|
+
*/
|
|
78
|
+
export interface FormSpaceSubmissionNotificationTaskUniqueIdInput {
|
|
79
|
+
readonly formSpaceId: FormSpaceId;
|
|
80
|
+
/**
|
|
81
|
+
* The attempt's reopen count. Defaults to 0, the first submission.
|
|
82
|
+
*/
|
|
83
|
+
readonly reopenCount?: Maybe<number>;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Returns the unique NotificationTask document id processing one SUBMISSION ATTEMPT of a FormSpace.
|
|
87
|
+
*
|
|
88
|
+
* The first attempt is the bare {@link notificationTaskUniqueId}, unsuffixed, so a space submitted before
|
|
89
|
+
* reopening existed keeps the exact document it already has. Every attempt after a reopen appends its
|
|
90
|
+
* count, which is what stops a resubmit from colliding with the finished task of the attempt before it.
|
|
91
|
+
*
|
|
92
|
+
* The `r` between the separators keeps the suffix from ever being able to produce a `__` run, whatever a
|
|
93
|
+
* FormSpace id happens to contain — Firestore reserves ids wrapped in double underscores, and staying well
|
|
94
|
+
* clear of them costs one character.
|
|
95
|
+
*
|
|
96
|
+
* Exported so a caller that has to LOCATE an attempt's task — a test asserting which document a resubmit
|
|
97
|
+
* created, or tooling inspecting a stuck submission — derives the id the same way the template does. The
|
|
98
|
+
* reopen action deliberately does not need it: the superseded attempt's task is left alone, either already
|
|
99
|
+
* done and awaiting the cleanup sweep or still queued and about to fence itself off.
|
|
100
|
+
*
|
|
101
|
+
* @param input - The FormSpace id and the attempt's reopen count.
|
|
102
|
+
* @returns The unique notification task id.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```ts
|
|
106
|
+
* formSpaceSubmissionNotificationTaskUniqueId({ formSpaceId: 'abc' }); // 'abc_FSPS'
|
|
107
|
+
* formSpaceSubmissionNotificationTaskUniqueId({ formSpaceId: 'abc', reopenCount: 2 }); // 'abc_FSPS_r2'
|
|
108
|
+
* ```
|
|
109
|
+
*
|
|
110
|
+
* @__NO_SIDE_EFFECTS__
|
|
111
|
+
*/
|
|
112
|
+
export declare function formSpaceSubmissionNotificationTaskUniqueId(input: FormSpaceSubmissionNotificationTaskUniqueIdInput): NotificationTaskUniqueId;
|
|
54
113
|
/**
|
|
55
114
|
* Creates a {@link CreateNotificationTaskTemplate} for a FormSpace submission task.
|
|
56
115
|
*
|
|
57
|
-
* The task is UNIQUE to
|
|
58
|
-
* resolve to the same document rather than racing a second processor against the
|
|
116
|
+
* The task is UNIQUE to one submission ATTEMPT of the FormSpace. Within an attempt that uniqueness is what
|
|
117
|
+
* makes a second template resolve to the same document rather than racing a second processor against the
|
|
118
|
+
* first; across attempts, {@link formSpaceSubmissionNotificationTaskUniqueId} keys them apart so a resubmit
|
|
119
|
+
* after a reopen gets a document — and a checkpoint flow — of its own.
|
|
59
120
|
*
|
|
60
|
-
* @param input - The target FormSpaceDocument and optional subtask data.
|
|
121
|
+
* @param input - The target FormSpaceDocument, its reopen count, and optional subtask data.
|
|
61
122
|
* @returns A CreateNotificationTaskTemplate for the submission task.
|
|
62
123
|
*
|
|
63
124
|
* @example
|
|
64
125
|
* ```ts
|
|
65
|
-
* const template = formSpaceSubmissionNotificationTaskTemplate({ formSpaceDocument: doc });
|
|
126
|
+
* const template = formSpaceSubmissionNotificationTaskTemplate({ formSpaceDocument: doc, reopenCount: formSpace.rc });
|
|
66
127
|
* ```
|
|
67
128
|
*/
|
|
68
129
|
export declare function formSpaceSubmissionNotificationTaskTemplate(input: FormSpaceSubmissionNotificationTaskInput): CreateNotificationTaskTemplate;
|
|
@@ -153,6 +153,39 @@ export interface FormSpaceTypeConfig {
|
|
|
153
153
|
* the sweep's inequality query.
|
|
154
154
|
*/
|
|
155
155
|
readonly expiresIn?: Maybe<Milliseconds>;
|
|
156
|
+
/**
|
|
157
|
+
* How long after EACH submission a space of this type may be reopened, returning it to an editable draft.
|
|
158
|
+
*
|
|
159
|
+
* THE MASTER SWITCH. When absent the type is never reopenable and submission stays the one-way door it
|
|
160
|
+
* has always been — which is what makes the whole reopen feature opt-in, and why no existing type can
|
|
161
|
+
* acquire resubmit semantics by accident.
|
|
162
|
+
*
|
|
163
|
+
* Distinct from {@link expiresIn}, which bounds the lifetime of the DRAFT. This bounds the lifetime of
|
|
164
|
+
* the SUBMISSION's mutability.
|
|
165
|
+
*
|
|
166
|
+
* Rolling by default: absent {@link reopenableUntil}, every submission grants a fresh window measured
|
|
167
|
+
* from its own `sat`.
|
|
168
|
+
*/
|
|
169
|
+
readonly reopenableFor?: Maybe<Milliseconds>;
|
|
170
|
+
/**
|
|
171
|
+
* A hard ceiling on reopening, measured from the FIRST submission rather than the current one.
|
|
172
|
+
*
|
|
173
|
+
* Materialized onto the space as `lat` on its first submit and never moved, so repeated reopen/resubmit
|
|
174
|
+
* rounds cannot walk the deadline forward the way a purely rolling {@link reopenableFor} lets them. Use
|
|
175
|
+
* it when "this submission is final N hours after it was first made" has to be a wall-clock guarantee.
|
|
176
|
+
*
|
|
177
|
+
* Absent leaves {@link reopenableFor} rolling. Meaningless on its own: {@link reopenableFor} is what
|
|
178
|
+
* permits a reopen at all, and this only narrows it.
|
|
179
|
+
*/
|
|
180
|
+
readonly reopenableUntil?: Maybe<Milliseconds>;
|
|
181
|
+
/**
|
|
182
|
+
* How many times a space of this type may be reopened. Counted against the space's monotonic `rc`.
|
|
183
|
+
*
|
|
184
|
+
* Absent leaves the count unbounded and the windows above the only limit — which is fine for a
|
|
185
|
+
* {@link reopenableUntil} ceiling, and worth setting deliberately for a purely rolling window, where a
|
|
186
|
+
* resubmit keeps earning a new one.
|
|
187
|
+
*/
|
|
188
|
+
readonly maxReopens?: Maybe<number>;
|
|
156
189
|
}
|
|
157
190
|
/**
|
|
158
191
|
* Default for {@link FormSpaceTypeConfig.maxUploads}.
|
|
@@ -89,18 +89,61 @@ export interface FormSpaceTemplateInput<T extends FormSpaceData = FormSpaceData>
|
|
|
89
89
|
* @__NO_SIDE_EFFECTS__
|
|
90
90
|
*/
|
|
91
91
|
export declare function formSpaceTemplate<T extends FormSpaceData = FormSpaceData>(input: FormSpaceTemplateInput<T>): FormSpace<T>;
|
|
92
|
+
/**
|
|
93
|
+
* Input for {@link submitFormSpaceTemplate}.
|
|
94
|
+
*/
|
|
95
|
+
export interface SubmitFormSpaceTemplateInput {
|
|
96
|
+
/**
|
|
97
|
+
* The space being submitted. Read to tell a FIRST submission from a resubmission after a reopen.
|
|
98
|
+
*/
|
|
99
|
+
readonly formSpace: Pick<FormSpace, 'fsat'>;
|
|
100
|
+
readonly config: FormSpaceTypeConfig;
|
|
101
|
+
/**
|
|
102
|
+
* The submission instant. Defaults to now.
|
|
103
|
+
*/
|
|
104
|
+
readonly now?: Maybe<Date>;
|
|
105
|
+
}
|
|
92
106
|
/**
|
|
93
107
|
* Builds the update template that submits a FormSpace.
|
|
94
108
|
*
|
|
95
109
|
* Clearing `eat` is not tidiness: a submitted space that kept its expiration instant would still match the
|
|
96
110
|
* expiration sweep and be retired out from under the processing task.
|
|
97
111
|
*
|
|
98
|
-
*
|
|
112
|
+
* `fsat` and the lock deadline `lat` are written ONLY on the first submission and are left untouched by a
|
|
113
|
+
* resubmission. That asymmetry is the whole first-submit anchor: recomputing `lat` here would let a
|
|
114
|
+
* reopen/resubmit round walk the deadline forward indefinitely, which is precisely what
|
|
115
|
+
* {@link FormSpaceTypeConfig.reopenableUntil} exists to prevent.
|
|
116
|
+
*
|
|
117
|
+
* @param input - The space, its type config, and the submission instant.
|
|
99
118
|
* @returns The update template.
|
|
100
119
|
*
|
|
101
120
|
* @__NO_SIDE_EFFECTS__
|
|
102
121
|
*/
|
|
103
|
-
export declare function submitFormSpaceTemplate(
|
|
122
|
+
export declare function submitFormSpaceTemplate(input: SubmitFormSpaceTemplateInput): Partial<FormSpace>;
|
|
123
|
+
/**
|
|
124
|
+
* Input for {@link resolveFormSpaceLocksAt}.
|
|
125
|
+
*/
|
|
126
|
+
export interface ResolveFormSpaceLocksAtInput {
|
|
127
|
+
readonly config: FormSpaceTypeConfig;
|
|
128
|
+
/**
|
|
129
|
+
* The instant the space was first submitted — the anchor the ceiling is measured from.
|
|
130
|
+
*/
|
|
131
|
+
readonly firstSubmittedAt: Date;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Returns the instant a submitted FormSpace of the given type becomes permanently locked, or null when its
|
|
135
|
+
* type declares no ceiling.
|
|
136
|
+
*
|
|
137
|
+
* The mirror of {@link resolveFormSpaceExpiresAt}, and null is meaningful in the same way: it is what
|
|
138
|
+
* leaves `lat` unwritten, and an unwritten `lat` is what leaves the type's `reopenableFor` rolling from
|
|
139
|
+
* each submission rather than capped.
|
|
140
|
+
*
|
|
141
|
+
* @param input - The type config and the first-submission instant.
|
|
142
|
+
* @returns The lock instant, or null when the type declares no ceiling.
|
|
143
|
+
*
|
|
144
|
+
* @__NO_SIDE_EFFECTS__
|
|
145
|
+
*/
|
|
146
|
+
export declare function resolveFormSpaceLocksAt(input: ResolveFormSpaceLocksAtInput): Maybe<Date>;
|
|
104
147
|
/**
|
|
105
148
|
* Builds the update template that expires a FormSpace.
|
|
106
149
|
*
|
|
@@ -132,6 +175,129 @@ export interface IsFormSpaceEditableInput {
|
|
|
132
175
|
* @__NO_SIDE_EFFECTS__
|
|
133
176
|
*/
|
|
134
177
|
export declare function isFormSpaceEditable(input: IsFormSpaceEditableInput): boolean;
|
|
178
|
+
/**
|
|
179
|
+
* Input for {@link isFormSpaceReopenable}.
|
|
180
|
+
*/
|
|
181
|
+
export interface IsFormSpaceReopenableInput {
|
|
182
|
+
readonly formSpace: Pick<FormSpace, 's' | 'sat' | 'lat' | 'rc'>;
|
|
183
|
+
readonly config: FormSpaceTypeConfig;
|
|
184
|
+
/**
|
|
185
|
+
* The instant to judge against. Defaults to now.
|
|
186
|
+
*/
|
|
187
|
+
readonly now?: Maybe<Date>;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Returns true when a submitted FormSpace may still be reopened into an editable draft.
|
|
191
|
+
*
|
|
192
|
+
* POLICY ONLY. It answers "does the type still allow this", not "is right now a safe moment" — a space
|
|
193
|
+
* whose processor is mid-run is reopenable by this predicate and refused by the action, because
|
|
194
|
+
* `ps === PROCESSING` is transient and telling a user their space is permanently locked while a task
|
|
195
|
+
* finishes would be a lie. The action owns that check; this owns the window.
|
|
196
|
+
*
|
|
197
|
+
* Requires SUBMITTED, which is what keeps EXPIRED and ARCHIVED terminal for free.
|
|
198
|
+
*
|
|
199
|
+
* @param input - The space, its type config, and the instant to judge against.
|
|
200
|
+
* @returns True when the space may be reopened.
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* ```ts
|
|
204
|
+
* const canReopen = isFormSpaceReopenable({ formSpace, config });
|
|
205
|
+
* ```
|
|
206
|
+
*
|
|
207
|
+
* @__NO_SIDE_EFFECTS__
|
|
208
|
+
*/
|
|
209
|
+
export declare function isFormSpaceReopenable(input: IsFormSpaceReopenableInput): boolean;
|
|
210
|
+
/**
|
|
211
|
+
* Input for {@link isFormSpaceFullyLocked}.
|
|
212
|
+
*/
|
|
213
|
+
export interface IsFormSpaceFullyLockedInput {
|
|
214
|
+
readonly formSpace: Pick<FormSpace, 's' | 'sat' | 'eat' | 'lat' | 'rc'>;
|
|
215
|
+
readonly config: FormSpaceTypeConfig;
|
|
216
|
+
/**
|
|
217
|
+
* The instant to judge against. Defaults to now.
|
|
218
|
+
*/
|
|
219
|
+
readonly now?: Maybe<Date>;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Returns true when nothing further can be done to a FormSpace: it is neither editable nor reopenable.
|
|
223
|
+
*
|
|
224
|
+
* Derived from the other two predicates rather than testing the fields itself, so the three answers can
|
|
225
|
+
* never disagree about one space. There is deliberately no FULLY_LOCKED {@link FormSpaceState} — a new
|
|
226
|
+
* enum member would fork every `s === SUBMITTED` check in the framework and downstream, to express
|
|
227
|
+
* something both existing predicates already know.
|
|
228
|
+
*
|
|
229
|
+
* @param input - The space, its type config, and the instant to judge against.
|
|
230
|
+
* @returns True when the space is fully locked.
|
|
231
|
+
*
|
|
232
|
+
* @__NO_SIDE_EFFECTS__
|
|
233
|
+
*/
|
|
234
|
+
export declare function isFormSpaceFullyLocked(input: IsFormSpaceFullyLockedInput): boolean;
|
|
235
|
+
/**
|
|
236
|
+
* Input for {@link reopenFormSpaceTemplate}.
|
|
237
|
+
*/
|
|
238
|
+
export interface ReopenFormSpaceTemplateInput {
|
|
239
|
+
readonly formSpace: Pick<FormSpace, 'rc' | 'lat'>;
|
|
240
|
+
readonly config: FormSpaceTypeConfig;
|
|
241
|
+
/**
|
|
242
|
+
* The user reopening the space, recorded on `rby`.
|
|
243
|
+
*/
|
|
244
|
+
readonly uid?: Maybe<FirebaseAuthUserId>;
|
|
245
|
+
/**
|
|
246
|
+
* The reopen instant. Defaults to now.
|
|
247
|
+
*/
|
|
248
|
+
readonly now?: Maybe<Date>;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Builds the update template that reopens a submitted FormSpace into an editable draft.
|
|
252
|
+
*
|
|
253
|
+
* It has to undo all THREE of {@link isFormSpaceEditable}'s conditions rather than just the state: a
|
|
254
|
+
* template that moved `s` back to DRAFT while leaving `sat` set, or leaving `eat` at the null submit wrote,
|
|
255
|
+
* produces a "draft" that either nothing can edit or nothing can ever retire.
|
|
256
|
+
*
|
|
257
|
+
* `eat` is re-armed to the EARLIER of a fresh `expiresIn` window and the space's own lock deadline, so the
|
|
258
|
+
* reopened draft can never outlive the window it was reopened inside. When the type declares neither, `eat`
|
|
259
|
+
* stays absent and the draft does not expire — the same bargain a type with no `expiresIn` already makes
|
|
260
|
+
* for a freshly created space.
|
|
261
|
+
*
|
|
262
|
+
* `uc` and `fi` are deliberately NOT rewound. `uc` bounds uploads ACCEPTED over the space's lifetime, so
|
|
263
|
+
* refunding it here would turn `maxUploads` into a bound on files retained that a reopen loop could evade;
|
|
264
|
+
* `fi` must never hand out an index twice. A type that expects replacement uploads has to budget
|
|
265
|
+
* `maxUploads` for them. `fsat` is likewise preserved — it is the record a reopen exists to not destroy.
|
|
266
|
+
*
|
|
267
|
+
* @param input - The space, its type config, the acting user, and the reopen instant.
|
|
268
|
+
* @returns The update template.
|
|
269
|
+
*
|
|
270
|
+
* @__NO_SIDE_EFFECTS__
|
|
271
|
+
*/
|
|
272
|
+
export declare function reopenFormSpaceTemplate(input: ReopenFormSpaceTemplateInput): Partial<FormSpace>;
|
|
273
|
+
/**
|
|
274
|
+
* Input for {@link lockFormSpaceTemplate}.
|
|
275
|
+
*/
|
|
276
|
+
export interface LockFormSpaceTemplateInput {
|
|
277
|
+
/**
|
|
278
|
+
* The user locking the space, recorded on `lby`.
|
|
279
|
+
*/
|
|
280
|
+
readonly uid?: Maybe<FirebaseAuthUserId>;
|
|
281
|
+
/**
|
|
282
|
+
* The lock instant. Defaults to now.
|
|
283
|
+
*/
|
|
284
|
+
readonly now?: Maybe<Date>;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Builds the update template that locks a submitted FormSpace's submission immediately.
|
|
288
|
+
*
|
|
289
|
+
* Only `lat` moves. The lock is not a state transition — the space stays SUBMITTED and its processing is
|
|
290
|
+
* untouched — it is the end of the reopen window, brought forward from whatever the type's
|
|
291
|
+
* `reopenableUntil` would have made it. Writing `lat` in the past is what makes every reopen predicate
|
|
292
|
+
* answer false from this instant on, including for a type whose window was purely rolling and so never
|
|
293
|
+
* had a `lat` at all.
|
|
294
|
+
*
|
|
295
|
+
* @param input - The acting user and the lock instant.
|
|
296
|
+
* @returns The update template.
|
|
297
|
+
*
|
|
298
|
+
* @__NO_SIDE_EFFECTS__
|
|
299
|
+
*/
|
|
300
|
+
export declare function lockFormSpaceTemplate(input: LockFormSpaceTemplateInput): Partial<FormSpace>;
|
|
135
301
|
/**
|
|
136
302
|
* Returns the slot config a type declares for the given slot, or null when it declares none.
|
|
137
303
|
*
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
* 2. Server picks it up via the send queue and routes to the registered handler
|
|
13
13
|
* 3. Handler returns a result indicating completion, partial progress, delay, or failure
|
|
14
14
|
* 4. Server updates the notification document accordingly and re-queues if not done
|
|
15
|
-
* 5. On completion (`true`), the notification document is deleted
|
|
15
|
+
* 5. On completion (`true`), the notification document is marked done (`d`) — NOT deleted. It stops
|
|
16
|
+
* matching the send queue immediately, but the document itself lingers until the cleanup sweep collects
|
|
17
|
+
* it, so a caller re-deriving a unique task's id can still find the finished one sitting there.
|
|
16
18
|
*/
|
|
17
19
|
import { type NotificationItem, type NotificationItemMetadata } from './notification.item';
|
|
18
20
|
import { type NotificationTaskType } from './notification.id';
|