@civitai/sdk 0.19.0 → 0.20.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.
- package/README.md +14 -6
- package/dist/app/index.d.ts +1 -1
- package/dist/app/index.js +6 -2
- package/dist/core/transport.d.ts +9 -0
- package/dist/core/transport.js +11 -0
- package/dist/doc-store/local-preview.d.ts +10 -0
- package/dist/doc-store/local-preview.js +12 -0
- package/dist/host/index.d.ts +9 -1
- package/dist/host/index.js +10 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.js +1 -1
- package/dist/workflows/index.d.ts +29 -0
- package/dist/workflows/index.js +22 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -559,7 +559,7 @@ A `kind: 'workflow'` body runs several steps as **one** workflow, and a later
|
|
|
559
559
|
step can consume an earlier step's result through a `{ $ref, path }` reference:
|
|
560
560
|
|
|
561
561
|
```ts
|
|
562
|
-
import { initialize, type WorkflowBody } from '@civitai/sdk';
|
|
562
|
+
import { estimatedCost, initialize, type WorkflowBody } from '@civitai/sdk';
|
|
563
563
|
|
|
564
564
|
const app = await initialize();
|
|
565
565
|
|
|
@@ -587,11 +587,13 @@ const body: WorkflowBody = {
|
|
|
587
587
|
|
|
588
588
|
// 1. Price it. Nothing is charged or reserved.
|
|
589
589
|
const estimate = await app.workflows.estimate(body);
|
|
590
|
-
|
|
590
|
+
// A priced estimate is not 'succeeded' ('pending' in practice): read the price, not the status.
|
|
591
|
+
const price = estimatedCost(estimate);
|
|
592
|
+
if (price === null) {
|
|
591
593
|
// A refusal RESOLVES — see below.
|
|
592
594
|
throw new Error(estimate.error ?? 'This workflow could not be priced.');
|
|
593
595
|
}
|
|
594
|
-
if (!(await confirmSpend(
|
|
596
|
+
if (!(await confirmSpend(price))) throw new Error('Not confirmed.');
|
|
595
597
|
|
|
596
598
|
// 2. Run it. One key per press of the button, reused on every retry.
|
|
597
599
|
const idempotencyKey = crypto.randomUUID();
|
|
@@ -617,7 +619,7 @@ submitted. `app.workflows` has no call for that confirmation step.
|
|
|
617
619
|
|
|
618
620
|
| Call | Does |
|
|
619
621
|
|---|---|
|
|
620
|
-
| `estimate(body)` | Prices the body. `snapshot
|
|
622
|
+
| `estimate(body)` | Prices the body. `estimatedCost(snapshot)` is the price in whole Buzz, or `null` when there is none |
|
|
621
623
|
| `submit(body, { idempotencyKey })` | Runs it, spending the viewer's Buzz |
|
|
622
624
|
| `get(workflowId)` | The workflow as it stands now |
|
|
623
625
|
| `watch(workflowId)` | An async iterator: the workflow now, then each time it changes, until a final status |
|
|
@@ -633,13 +635,19 @@ branch on the snapshot rather than wrapping the call in `try`:
|
|
|
633
635
|
|
|
634
636
|
```ts
|
|
635
637
|
const estimate = await app.workflows.estimate(body);
|
|
636
|
-
if (estimate
|
|
638
|
+
if (estimatedCost(estimate) === null) {
|
|
637
639
|
estimate.error; // a sentence for the viewer, e.g. which step is at fault
|
|
638
640
|
estimate.errorCode; // a stable code to branch on, when the refusal defines one
|
|
639
|
-
estimate.cost; // absent: nothing was priced
|
|
640
641
|
}
|
|
641
642
|
```
|
|
642
643
|
|
|
644
|
+
`estimatedCost` is the check to use: the price when the estimate has one, else
|
|
645
|
+
`null`. An estimate is priced when its `status` is not `'failed'` and it carries
|
|
646
|
+
a numeric `cost.total`. 🔴 **Do not require `'succeeded'`**: nothing runs on an
|
|
647
|
+
estimate, so a priced one comes back with a status that has not finished
|
|
648
|
+
(`'pending'` in practice), and a check for `'succeeded'` treats every price as
|
|
649
|
+
missing. `0` is a price.
|
|
650
|
+
|
|
643
651
|
Three things follow from that shape:
|
|
644
652
|
|
|
645
653
|
- **A refused estimate has no `cost`.** An estimate the host could not price
|
package/dist/app/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BlockContext, BlockSettings, Theme, ViewerInfo } from '../core/handshake.js';
|
|
2
|
-
import type
|
|
2
|
+
import { type BlockTransport } from '../core/transport.js';
|
|
3
3
|
import { type Host } from '../host/index.js';
|
|
4
4
|
import { type OrchestrationClient } from '../orchestration/index.js';
|
|
5
5
|
import { type GrantOptions, type Scope, type TokenOptions, type TokenSessionOptions } from '../session/index.js';
|
package/dist/app/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BridgeError, CivitaiError } from '../core/errors.js';
|
|
2
2
|
import { getTransport } from '../core/get-transport.js';
|
|
3
|
+
import { isLocalPreviewStorage } from '../core/transport.js';
|
|
3
4
|
import { createHost, createHostSession } from '../host/index.js';
|
|
4
5
|
import { ApiError, createHttp } from '../http/index.js';
|
|
5
6
|
import { createOrchestrationClient, DEFAULT_ORCHESTRATION_URL, } from '../orchestration/index.js';
|
|
@@ -26,13 +27,16 @@ export async function initialize(options = {}) {
|
|
|
26
27
|
// from `token.scopes`. A published app whose manifest forgot `apps:storage`
|
|
27
28
|
// holds a token that looks the same as a preview's, and must keep getting the
|
|
28
29
|
// real route's `insufficient_scope` rather than a silent local store.
|
|
29
|
-
const
|
|
30
|
+
const localPreview = isLocalPreviewStorage(snapshot());
|
|
31
|
+
// The same latched answer configures the session below, so `requestGrants`
|
|
32
|
+
// and the store agree on whether the document store is local.
|
|
33
|
+
const local = localPreview
|
|
30
34
|
? // The manifest arrives only from `blockManifestPlugin()`'s dev-server
|
|
31
35
|
// global: there is deliberately no option to pass one.
|
|
32
36
|
await createLocalPreviewStorage(readInjectedManifest(), snapshot().viewer)
|
|
33
37
|
: undefined;
|
|
34
38
|
return {
|
|
35
|
-
...createAppClient(createHostSession(transport), options, holdsBlockTokenNow, holdsOAuthTokenNow, local),
|
|
39
|
+
...createAppClient(createHostSession(transport, { localPreviewStorage: localPreview }), options, holdsBlockTokenNow, holdsOAuthTokenNow, local),
|
|
36
40
|
host: createHost(transport),
|
|
37
41
|
get viewer() {
|
|
38
42
|
return snapshot().viewer;
|
package/dist/core/transport.d.ts
CHANGED
|
@@ -63,6 +63,15 @@ export interface BlockTransport {
|
|
|
63
63
|
on(type: string, handler: (payload: unknown) => void): () => void;
|
|
64
64
|
}
|
|
65
65
|
export declare const EMPTY_SNAPSHOT: BlockSnapshot;
|
|
66
|
+
/**
|
|
67
|
+
* Did the host mark this frame a dev-tunnel preview whose document store is
|
|
68
|
+
* served locally? THE predicate for that question. `initialize()` reads it once
|
|
69
|
+
* and the one latched answer selects the local document store AND tells the
|
|
70
|
+
* host session which scopes no grant can add (`requestGrants`), so the two
|
|
71
|
+
* cannot disagree. Never inferred from `token.scopes`: a published app whose
|
|
72
|
+
* token lacks `apps:storage` looks the same as a preview's.
|
|
73
|
+
*/
|
|
74
|
+
export declare function isLocalPreviewStorage(snapshot: Pick<BlockSnapshot, 'storageMode'>): boolean;
|
|
66
75
|
/** Convert a wire `BlockInitPayload` (ISO string expiresAt) into a `BlockSnapshot`. */
|
|
67
76
|
export declare function snapshotFromInit(payload: BlockInitPayload, hostOrigin: string): BlockSnapshot;
|
|
68
77
|
/** Wire `WrappedToken` (ISO expiry) → runtime `BlockToken` (Date expiry). */
|
package/dist/core/transport.js
CHANGED
|
@@ -10,6 +10,17 @@ export const EMPTY_SNAPSHOT = {
|
|
|
10
10
|
blockInstanceId: '',
|
|
11
11
|
hostOrigin: null,
|
|
12
12
|
};
|
|
13
|
+
/**
|
|
14
|
+
* Did the host mark this frame a dev-tunnel preview whose document store is
|
|
15
|
+
* served locally? THE predicate for that question. `initialize()` reads it once
|
|
16
|
+
* and the one latched answer selects the local document store AND tells the
|
|
17
|
+
* host session which scopes no grant can add (`requestGrants`), so the two
|
|
18
|
+
* cannot disagree. Never inferred from `token.scopes`: a published app whose
|
|
19
|
+
* token lacks `apps:storage` looks the same as a preview's.
|
|
20
|
+
*/
|
|
21
|
+
export function isLocalPreviewStorage(snapshot) {
|
|
22
|
+
return snapshot.storageMode === LOCAL_PREVIEW_STORAGE;
|
|
23
|
+
}
|
|
13
24
|
/** Convert a wire `BlockInitPayload` (ISO string expiresAt) into a `BlockSnapshot`. */
|
|
14
25
|
export function snapshotFromInit(payload, hostOrigin) {
|
|
15
26
|
return {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ViewerInfo } from '../core/handshake.js';
|
|
2
|
+
import type { Scope } from '../session/index.js';
|
|
2
3
|
import { type DocumentStorage } from './index.js';
|
|
3
4
|
/**
|
|
4
5
|
* The document store for a DEV-TUNNEL PREVIEW: the host marked the frame
|
|
@@ -22,6 +23,15 @@ import { type DocumentStorage } from './index.js';
|
|
|
22
23
|
* `tests/guards/local-preview-manifest-global.test.mjs`.
|
|
23
24
|
*/
|
|
24
25
|
export declare const LOCAL_PREVIEW_MANIFEST_GLOBAL = "__CIVITAI_LOCAL_PREVIEW_MANIFEST__";
|
|
26
|
+
/**
|
|
27
|
+
* The scopes the local document store serves: the two the server's document
|
|
28
|
+
* store checks (`fake.ts` enforces the same pair). A preview token never
|
|
29
|
+
* carries them and no grant can add them, so `requestGrants` treats them as
|
|
30
|
+
* held in a local preview instead of asking the host. Shared storage
|
|
31
|
+
* (`apps:storage:shared:*`) and the deprecated key/value scopes are NOT here:
|
|
32
|
+
* they keep the real routes in a preview.
|
|
33
|
+
*/
|
|
34
|
+
export declare const LOCAL_PREVIEW_STORAGE_SCOPES: ReadonlySet<Scope>;
|
|
25
35
|
/** What a local preview reads from the manifest: the scopes it declares and its `storage` block. */
|
|
26
36
|
export interface LocalPreviewManifest {
|
|
27
37
|
scopes?: readonly string[];
|
|
@@ -21,6 +21,18 @@ import { createDocumentStorage, StorageError } from './index.js';
|
|
|
21
21
|
* `tests/guards/local-preview-manifest-global.test.mjs`.
|
|
22
22
|
*/
|
|
23
23
|
export const LOCAL_PREVIEW_MANIFEST_GLOBAL = '__CIVITAI_LOCAL_PREVIEW_MANIFEST__';
|
|
24
|
+
/**
|
|
25
|
+
* The scopes the local document store serves: the two the server's document
|
|
26
|
+
* store checks (`fake.ts` enforces the same pair). A preview token never
|
|
27
|
+
* carries them and no grant can add them, so `requestGrants` treats them as
|
|
28
|
+
* held in a local preview instead of asking the host. Shared storage
|
|
29
|
+
* (`apps:storage:shared:*`) and the deprecated key/value scopes are NOT here:
|
|
30
|
+
* they keep the real routes in a preview.
|
|
31
|
+
*/
|
|
32
|
+
export const LOCAL_PREVIEW_STORAGE_SCOPES = new Set([
|
|
33
|
+
'apps:storage',
|
|
34
|
+
'apps:storage:publish',
|
|
35
|
+
]);
|
|
24
36
|
/** The manifest the dev server injected, if any. */
|
|
25
37
|
export function readInjectedManifest() {
|
|
26
38
|
const value = globalThis[LOCAL_PREVIEW_MANIFEST_GLOBAL];
|
package/dist/host/index.d.ts
CHANGED
|
@@ -124,5 +124,13 @@ export interface Host {
|
|
|
124
124
|
}, opts?: HostCallOptions): Promise<number[]>;
|
|
125
125
|
}
|
|
126
126
|
export declare function createHost(transport: BlockTransport): Host;
|
|
127
|
+
export interface HostSessionOptions {
|
|
128
|
+
/**
|
|
129
|
+
* The document store is served by the in-tab local store (a dev-tunnel
|
|
130
|
+
* preview): the value `initialize()` latched from `isLocalPreviewStorage`.
|
|
131
|
+
* `requestGrants` then treats `LOCAL_PREVIEW_STORAGE_SCOPES` as held.
|
|
132
|
+
*/
|
|
133
|
+
localPreviewStorage?: boolean;
|
|
134
|
+
}
|
|
127
135
|
/** The token the host minted for this block, kept current as the host rotates it. */
|
|
128
|
-
export declare function createHostSession(transport: BlockTransport): Session;
|
|
136
|
+
export declare function createHostSession(transport: BlockTransport, { localPreviewStorage }?: HostSessionOptions): Session;
|
package/dist/host/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BridgeError } from '../core/errors.js';
|
|
2
2
|
import { createCaller, createListener, createNotifier } from '../core/messaging.js';
|
|
3
3
|
import { tokenFromWrapped } from '../core/transport.js';
|
|
4
|
+
import { LOCAL_PREVIEW_STORAGE_SCOPES } from '../doc-store/local-preview.js';
|
|
4
5
|
const call = createCaller();
|
|
5
6
|
const notify = createNotifier();
|
|
6
7
|
const on = createListener();
|
|
@@ -254,7 +255,7 @@ function asScanResult(value) {
|
|
|
254
255
|
return { status: 'error', ...(typeof v?.message === 'string' ? { message: v.message } : {}) };
|
|
255
256
|
}
|
|
256
257
|
/** The token the host minted for this block, kept current as the host rotates it. */
|
|
257
|
-
export function createHostSession(transport) {
|
|
258
|
+
export function createHostSession(transport, { localPreviewStorage = false } = {}) {
|
|
258
259
|
return {
|
|
259
260
|
async getToken({ fresh, signal } = {}) {
|
|
260
261
|
if (!fresh)
|
|
@@ -263,7 +264,14 @@ export function createHostSession(transport) {
|
|
|
263
264
|
const { token } = await call('REQUEST_TOKEN', { blockInstanceId }, { transport, signal });
|
|
264
265
|
return tokenFromWrapped(token).raw;
|
|
265
266
|
},
|
|
266
|
-
requestGrants: (scopes, opts = {}) => requestGrants(transport,
|
|
267
|
+
requestGrants: (scopes, opts = {}) => requestGrants(transport,
|
|
268
|
+
// A preview token never carries a document-store scope and no grant can
|
|
269
|
+
// add one, so asking the host for it could only be refused — and the
|
|
270
|
+
// refusal would take the rest of the request down with it. The local
|
|
271
|
+
// store serves them; ask only for the rest. Nothing on the token changes.
|
|
272
|
+
localPreviewStorage
|
|
273
|
+
? scopes.filter((scope) => !LOCAL_PREVIEW_STORAGE_SCOPES.has(scope))
|
|
274
|
+
: scopes, opts),
|
|
267
275
|
};
|
|
268
276
|
}
|
|
269
277
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export { SubListingError } from './sub-listings/index.js';
|
|
|
15
15
|
export type { MySubListing, SubListingCallOptions, SubListingContentRating, SubListingErrorCode, SubListingStatus, SubListingsClient, SubListingUpsertInput, SubListingUpsertResult, SubListingWithdrawResult, } from './sub-listings/index.js';
|
|
16
16
|
export { StorageError } from './doc-store/index.js';
|
|
17
17
|
export type { DocumentAuthor, DocumentCallOptions, DocumentListQuery, DocumentPage, DocumentStorage, DocumentWhereValue, StorageCollection, StorageCollectionInfo, StorageDocument, StorageErrorCode, StorageIndexType, StorageInfo, StorageLimits, StorageUsage, WriteOptions, } from './doc-store/index.js';
|
|
18
|
-
export { SubmissionUnconfirmedError } from './workflows/index.js';
|
|
18
|
+
export { estimatedCost, SubmissionUnconfirmedError } from './workflows/index.js';
|
|
19
19
|
export type { WorkflowCallOptions, WorkflowGetOptions, WorkflowListQuery, WorkflowListResult, WorkflowSubmitOptions, WorkflowWatchOptions, WorkflowsClient, } from './workflows/index.js';
|
|
20
20
|
export type { AiToolkitLrScheduler, AiToolkitOptimizerType, AiToolkitPlainEcosystem, AiToolkitTrainingParams, AiToolkitTrainingParamsBase, AppWorkflow, AppWorkflowImage, BlockModel3dOutput, BlockModelSubstitution, BlockPublishedModel, BlockSourceImage, BlockStepQuote, BlockStepToolCall, BlockTextToImageParams, BlockTrainedEpoch, BlockTrainingDatasetItem, BlockTrainingQuote, BlockWorkflowSnapshot, BlockWorkflowStepSnapshot, BuzzAccountType, InlineComfyNode, KnownBlockWorkflowErrorCode, ModelSubstitutionReason, WithWorkflowStepRefs, WorkflowBody, WorkflowBodyCustomComfy, WorkflowBodyCustomComfyInline, WorkflowBodyCustomComfyRecipe, WorkflowBodyPassThroughStep, WorkflowBodyStep, WorkflowBodyTextToImage, WorkflowBodyTraining, WorkflowBodyWorkflow, WorkflowBodyWorkflowStep, WorkflowStepRef, } from './workflows/types.generated.js';
|
|
21
21
|
export { isTerminal, isTerminalStatus } from './orchestration/index.js';
|
package/dist/index.js
CHANGED
|
@@ -18,7 +18,7 @@ export { createSignIn, SignInError } from './sign-in/index.js';
|
|
|
18
18
|
export { SCOPES } from './session/index.js';
|
|
19
19
|
export { SubListingError } from './sub-listings/index.js';
|
|
20
20
|
export { StorageError } from './doc-store/index.js';
|
|
21
|
-
export { SubmissionUnconfirmedError } from './workflows/index.js';
|
|
21
|
+
export { estimatedCost, SubmissionUnconfirmedError } from './workflows/index.js';
|
|
22
22
|
export { isTerminal, isTerminalStatus } from './orchestration/index.js';
|
|
23
23
|
export { createHost } from './host/index.js';
|
|
24
24
|
export { ApiError } from './http/index.js';
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** This package's version, reported to the host as `sdkVersion` in `BLOCK_READY`. */
|
|
2
|
-
export declare const VERSION = "0.
|
|
2
|
+
export declare const VERSION = "0.20.0";
|
|
@@ -28,6 +28,25 @@ export declare class SubmissionUnconfirmedError extends CivitaiError {
|
|
|
28
28
|
readonly snapshot: BlockWorkflowSnapshot | undefined;
|
|
29
29
|
constructor(snapshot: BlockWorkflowSnapshot | undefined);
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* The price an {@link WorkflowsClient.estimate} reply quotes, in whole Buzz, or
|
|
33
|
+
* `null` when it quotes none.
|
|
34
|
+
*
|
|
35
|
+
* An estimate is priced when its `status` is not `'failed'` AND it carries a
|
|
36
|
+
* numeric `cost.total`. That is the whole rule, and it is the one to branch on:
|
|
37
|
+
* - Do not require `'succeeded'`. Nothing runs on an estimate, so a priced one
|
|
38
|
+
* comes back with a status that has not finished (`'pending'` in practice),
|
|
39
|
+
* and a check for `'succeeded'` reads every price as missing.
|
|
40
|
+
* - `0` is a price.
|
|
41
|
+
* - A `'failed'` snapshot is never priced, even one carrying a `cost.total`: a
|
|
42
|
+
* budget refusal carries the total it would not charge.
|
|
43
|
+
* - A snapshot with no numeric `cost.total` is not priced, whatever its status.
|
|
44
|
+
*
|
|
45
|
+
* `null` does not predict a refused {@link WorkflowsClient.submit}: a body the
|
|
46
|
+
* host cannot price can still be submitted. Read `error` / `errorCode` on the
|
|
47
|
+
* snapshot for why.
|
|
48
|
+
*/
|
|
49
|
+
export declare function estimatedCost(snapshot: Pick<BlockWorkflowSnapshot, 'status' | 'cost'>): number | null;
|
|
31
50
|
export interface WorkflowCallOptions {
|
|
32
51
|
signal?: AbortSignal;
|
|
33
52
|
}
|
|
@@ -102,6 +121,16 @@ export interface WorkflowsClient {
|
|
|
102
121
|
* The price is `snapshot.cost.total`. A refused estimate resolves with
|
|
103
122
|
* `status: 'failed'` and no `cost`; a body the host cannot price fails this
|
|
104
123
|
* way without that predicting a refused {@link WorkflowsClient.submit}.
|
|
124
|
+
*
|
|
125
|
+
* 🔴 DO NOT REQUIRE `'succeeded'`. Nothing runs on an estimate, so a priced
|
|
126
|
+
* one comes back with a status that has not finished (`'pending'` in
|
|
127
|
+
* practice). Read the price with {@link estimatedCost}, which is `null`
|
|
128
|
+
* exactly when there is none to show:
|
|
129
|
+
*
|
|
130
|
+
* ```ts
|
|
131
|
+
* const price = estimatedCost(await app.workflows.estimate(body));
|
|
132
|
+
* if (price === null) showMessage('This generation could not be priced.');
|
|
133
|
+
* ```
|
|
105
134
|
*/
|
|
106
135
|
estimate(body: WorkflowBody, opts?: WorkflowCallOptions): Promise<BlockWorkflowSnapshot>;
|
|
107
136
|
/**
|
package/dist/workflows/index.js
CHANGED
|
@@ -51,6 +51,28 @@ export class SubmissionUnconfirmedError extends CivitaiError {
|
|
|
51
51
|
this.snapshot = snapshot;
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* The price an {@link WorkflowsClient.estimate} reply quotes, in whole Buzz, or
|
|
56
|
+
* `null` when it quotes none.
|
|
57
|
+
*
|
|
58
|
+
* An estimate is priced when its `status` is not `'failed'` AND it carries a
|
|
59
|
+
* numeric `cost.total`. That is the whole rule, and it is the one to branch on:
|
|
60
|
+
* - Do not require `'succeeded'`. Nothing runs on an estimate, so a priced one
|
|
61
|
+
* comes back with a status that has not finished (`'pending'` in practice),
|
|
62
|
+
* and a check for `'succeeded'` reads every price as missing.
|
|
63
|
+
* - `0` is a price.
|
|
64
|
+
* - A `'failed'` snapshot is never priced, even one carrying a `cost.total`: a
|
|
65
|
+
* budget refusal carries the total it would not charge.
|
|
66
|
+
* - A snapshot with no numeric `cost.total` is not priced, whatever its status.
|
|
67
|
+
*
|
|
68
|
+
* `null` does not predict a refused {@link WorkflowsClient.submit}: a body the
|
|
69
|
+
* host cannot price can still be submitted. Read `error` / `errorCode` on the
|
|
70
|
+
* snapshot for why.
|
|
71
|
+
*/
|
|
72
|
+
export function estimatedCost(snapshot) {
|
|
73
|
+
const total = snapshot.cost?.total;
|
|
74
|
+
return snapshot.status !== 'failed' && typeof total === 'number' ? total : null;
|
|
75
|
+
}
|
|
54
76
|
const isSnapshot = (value) => value != null && typeof value === 'object' && typeof value.status === 'string';
|
|
55
77
|
/**
|
|
56
78
|
* Asserted, not cast: every caller branches on `status`, and a reply without
|
package/package.json
CHANGED