@abloatai/transaction 0.55.0 → 0.57.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/CONVENTIONS.md +34 -0
- package/dist/auth/hostedEndpoints.d.ts +21 -5
- package/dist/auth/hostedEndpoints.d.ts.map +1 -1
- package/dist/auth/hostedEndpoints.js +21 -5
- package/dist/auth/hostedEndpoints.js.map +1 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +1 -1
- package/dist/auth/index.js.map +1 -1
- package/dist/coordination/index.d.ts +2 -2
- package/dist/coordination/index.d.ts.map +1 -1
- package/dist/coordination/index.js +1 -1
- package/dist/coordination/index.js.map +1 -1
- package/dist/coordination/schema.d.ts +0 -3
- package/dist/coordination/schema.d.ts.map +1 -1
- package/dist/coordination/schema.js +4 -5
- package/dist/coordination/schema.js.map +1 -1
- package/dist/errorCodes.d.ts +1 -0
- package/dist/errorCodes.d.ts.map +1 -1
- package/dist/errorCodes.js +1 -0
- package/dist/errorCodes.js.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +4 -1
- package/dist/errors.js.map +1 -1
- package/dist/log/syncDeltaRow.d.ts +3 -3
- package/dist/readSetContext.d.ts.map +1 -1
- package/dist/readSetContext.js +8 -3
- package/dist/readSetContext.js.map +1 -1
- package/dist/resources/httpResources.d.ts +43 -22
- package/dist/resources/httpResources.d.ts.map +1 -1
- package/dist/resources/httpResources.js +81 -4
- package/dist/resources/httpResources.js.map +1 -1
- package/dist/resources/modelCreate.d.ts +21 -0
- package/dist/resources/modelCreate.d.ts.map +1 -0
- package/dist/resources/modelCreate.js +49 -0
- package/dist/resources/modelCreate.js.map +1 -0
- package/dist/resources/modelOperations.d.ts +24 -2
- package/dist/resources/modelOperations.d.ts.map +1 -1
- package/dist/resources/modelOperations.js.map +1 -1
- package/dist/resources/writeOptionsSchema.d.ts +14 -0
- package/dist/resources/writeOptionsSchema.d.ts.map +1 -1
- package/dist/resources/writeOptionsSchema.js +23 -2
- package/dist/resources/writeOptionsSchema.js.map +1 -1
- package/dist/schema/audit.d.ts +15 -0
- package/dist/schema/audit.d.ts.map +1 -0
- package/dist/schema/audit.js +90 -0
- package/dist/schema/audit.js.map +1 -0
- package/dist/schema/ddl.d.ts.map +1 -1
- package/dist/schema/ddl.js +68 -1
- package/dist/schema/ddl.js.map +1 -1
- package/dist/schema/index.d.ts +3 -1
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +3 -1
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/model.d.ts +14 -0
- package/dist/schema/model.d.ts.map +1 -1
- package/dist/schema/model.js +2 -0
- package/dist/schema/model.js.map +1 -1
- package/dist/schema/openapi.d.ts.map +1 -1
- package/dist/schema/openapi.js +104 -2
- package/dist/schema/openapi.js.map +1 -1
- package/dist/schema/roles.d.ts +27 -0
- package/dist/schema/roles.d.ts.map +1 -1
- package/dist/schema/roles.js +40 -0
- package/dist/schema/roles.js.map +1 -1
- package/dist/schema/schema.d.ts +23 -4
- package/dist/schema/schema.d.ts.map +1 -1
- package/dist/schema/schema.js +30 -1
- package/dist/schema/schema.js.map +1 -1
- package/dist/schema/serialize.d.ts +4 -1
- package/dist/schema/serialize.d.ts.map +1 -1
- package/dist/schema/serialize.js +5 -1
- package/dist/schema/serialize.js.map +1 -1
- package/dist/schema/subject.d.ts +18 -0
- package/dist/schema/subject.d.ts.map +1 -0
- package/dist/schema/subject.js +27 -0
- package/dist/schema/subject.js.map +1 -0
- package/dist/server/adapter.d.ts +2 -0
- package/dist/server/adapter.d.ts.map +1 -1
- package/dist/server/readConfig.d.ts +3 -0
- package/dist/server/readConfig.d.ts.map +1 -1
- package/dist/server/readConfig.js +0 -21
- package/dist/server/readConfig.js.map +1 -1
- package/dist/source/adapters/drizzle.d.ts.map +1 -1
- package/dist/source/adapters/drizzle.js +37 -6
- package/dist/source/adapters/drizzle.js.map +1 -1
- package/dist/source/adapters/kysely.d.ts +2 -0
- package/dist/source/adapters/kysely.d.ts.map +1 -1
- package/dist/source/adapters/kysely.js +33 -5
- package/dist/source/adapters/kysely.js.map +1 -1
- package/dist/source/adapters/kyselyMutationCore.d.ts +4 -1
- package/dist/source/adapters/kyselyMutationCore.d.ts.map +1 -1
- package/dist/source/adapters/kyselyMutationCore.js +17 -8
- package/dist/source/adapters/kyselyMutationCore.js.map +1 -1
- package/dist/source/adapters/memory.d.ts.map +1 -1
- package/dist/source/adapters/memory.js +1 -0
- package/dist/source/adapters/memory.js.map +1 -1
- package/dist/source/adapters/prisma.d.ts.map +1 -1
- package/dist/source/adapters/prisma.js +59 -7
- package/dist/source/adapters/prisma.js.map +1 -1
- package/dist/source/contract.d.ts +14 -0
- package/dist/source/contract.d.ts.map +1 -1
- package/dist/source/contract.js +10 -0
- package/dist/source/contract.js.map +1 -1
- package/dist/source/factory.d.ts +7 -1
- package/dist/source/factory.d.ts.map +1 -1
- package/dist/source/factory.js +116 -5
- package/dist/source/factory.js.map +1 -1
- package/dist/source/index.d.ts +2 -1
- package/dist/source/index.d.ts.map +1 -1
- package/dist/source/index.js +1 -0
- package/dist/source/index.js.map +1 -1
- package/dist/source/migrations.d.ts.map +1 -1
- package/dist/source/migrations.js +21 -0
- package/dist/source/migrations.js.map +1 -1
- package/dist/source/subjectAuthorization.d.ts +16 -0
- package/dist/source/subjectAuthorization.d.ts.map +1 -0
- package/dist/source/subjectAuthorization.js +126 -0
- package/dist/source/subjectAuthorization.js.map +1 -0
- package/dist/source/types.d.ts +52 -2
- package/dist/source/types.d.ts.map +1 -1
- package/dist/source/types.js +1 -0
- package/dist/source/types.js.map +1 -1
- package/dist/syncLog/contract.d.ts +45 -3
- package/dist/syncLog/contract.d.ts.map +1 -1
- package/dist/syncLog/contract.js +46 -4
- package/dist/syncLog/contract.js.map +1 -1
- package/dist/testing/fixtures/httpResponses.d.ts +6 -0
- package/dist/testing/fixtures/httpResponses.d.ts.map +1 -1
- package/dist/testing/fixtures/httpResponses.js +1 -0
- package/dist/testing/fixtures/httpResponses.js.map +1 -1
- package/dist/transport/httpClient.d.ts +8 -1
- package/dist/transport/httpClient.d.ts.map +1 -1
- package/dist/transport/httpClient.js +51 -20
- package/dist/transport/httpClient.js.map +1 -1
- package/dist/transport/httpCommitRequest.d.ts +45 -0
- package/dist/transport/httpCommitRequest.d.ts.map +1 -0
- package/dist/transport/httpCommitRequest.js +67 -0
- package/dist/transport/httpCommitRequest.js.map +1 -0
- package/dist/transport/httpTransport.d.ts.map +1 -1
- package/dist/transport/httpTransport.js +69 -106
- package/dist/transport/httpTransport.js.map +1 -1
- package/dist/transport/httpTransportHelpers.d.ts +10 -0
- package/dist/transport/httpTransportHelpers.d.ts.map +1 -0
- package/dist/transport/httpTransportHelpers.js +46 -0
- package/dist/transport/httpTransportHelpers.js.map +1 -0
- package/dist/wire/apiLifecycle.d.ts +63 -0
- package/dist/wire/apiLifecycle.d.ts.map +1 -0
- package/dist/wire/apiLifecycle.js +89 -0
- package/dist/wire/apiLifecycle.js.map +1 -0
- package/dist/wire/auth.d.ts.map +1 -1
- package/dist/wire/auth.js +6 -3
- package/dist/wire/auth.js.map +1 -1
- package/dist/wire/commit.d.ts +74 -9
- package/dist/wire/commit.d.ts.map +1 -1
- package/dist/wire/commit.js +18 -5
- package/dist/wire/commit.js.map +1 -1
- package/dist/wire/index.d.ts +5 -2
- package/dist/wire/index.d.ts.map +1 -1
- package/dist/wire/index.js +12 -1
- package/dist/wire/index.js.map +1 -1
- package/dist/wire/modelMutations.js +3 -2
- package/dist/wire/modelMutations.js.map +1 -1
- package/dist/wire/rateLimit.d.ts +82 -0
- package/dist/wire/rateLimit.d.ts.map +1 -0
- package/dist/wire/rateLimit.js +142 -0
- package/dist/wire/rateLimit.js.map +1 -0
- package/package.json +1 -1
- package/src/auth/hostedEndpoints.ts +23 -5
- package/src/auth/index.ts +2 -0
- package/src/coordination/index.ts +0 -2
- package/src/coordination/schema.ts +4 -7
- package/src/errorCodes.ts +6 -0
- package/src/errors.ts +4 -1
- package/src/readSetContext.ts +8 -2
- package/src/resources/httpResources.ts +116 -25
- package/src/resources/modelCreate.ts +73 -0
- package/src/resources/modelOperations.ts +29 -2
- package/src/resources/writeOptionsSchema.ts +30 -2
- package/src/schema/audit.ts +121 -0
- package/src/schema/ddl.ts +71 -1
- package/src/schema/index.ts +16 -0
- package/src/schema/model.ts +17 -0
- package/src/schema/openapi.ts +123 -2
- package/src/schema/roles.ts +53 -0
- package/src/schema/schema.ts +68 -3
- package/src/schema/serialize.ts +8 -1
- package/src/schema/subject.ts +43 -0
- package/src/server/adapter.ts +2 -0
- package/src/server/readConfig.ts +4 -0
- package/src/source/adapters/drizzle.ts +50 -6
- package/src/source/adapters/kysely.ts +47 -4
- package/src/source/adapters/kyselyMutationCore.ts +21 -9
- package/src/source/adapters/memory.ts +1 -0
- package/src/source/adapters/prisma.ts +70 -7
- package/src/source/contract.ts +11 -0
- package/src/source/factory.ts +141 -5
- package/src/source/index.ts +6 -0
- package/src/source/migrations.ts +21 -0
- package/src/source/subjectAuthorization.ts +182 -0
- package/src/source/types.ts +56 -2
- package/src/syncLog/contract.ts +47 -5
- package/src/testing/fixtures/httpResponses.ts +7 -0
- package/src/transport/httpClient.ts +85 -23
- package/src/transport/httpCommitRequest.ts +104 -0
- package/src/transport/httpTransport.ts +92 -142
- package/src/transport/httpTransportHelpers.ts +63 -0
- package/src/wire/apiLifecycle.ts +94 -0
- package/src/wire/auth.ts +6 -3
- package/src/wire/commit.ts +20 -5
- package/src/wire/index.ts +31 -0
- package/src/wire/modelMutations.ts +2 -2
- package/src/wire/rateLimit.ts +155 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"modelMutations.js","sourceRoot":"","sources":["../../src/wire/modelMutations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,wBAAwB,EACxB,2BAA2B,EAC3B,
|
|
1
|
+
{"version":3,"file":"modelMutations.js","sourceRoot":"","sources":["../../src/wire/modelMutations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,wBAAwB,EACxB,2BAA2B,EAC3B,yBAAyB,GAC1B,MAAM,2BAA2B,CAAC;AAEnC,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,CAAC,MAAM,CAAC;IACjD,gFAAgF;IAChF,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE;IACjD;;;;;;;;;;;OAWG;IACH,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE;IACxB,gEAAgE;IAChE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE;IAC3B,2EAA2E;IAC3E,OAAO,EAAE,iBAAiB,CAAC,OAAO,EAAE;IACpC;;;;OAIG;IACH,MAAM,EAAE,iBAAiB,CAAC,OAAO,EAAE;IACnC,4EAA4E;IAC5E,KAAK,EAAE,wBAAwB,CAAC,OAAO,EAAE;IACzC,uEAAuE;IACvE,KAAK,EAAE,yBAAyB,CAAC,OAAO,EAAE;IAC1C;;;OAGG;IACH,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE;IAChC,wEAAwE;IACxE,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACtC,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,2BAA2B,CAAC,KAAK,CAAC,IAAI,oBAAoB,EAAE;IAC/E,IAAI,EAAE,CAAC,OAAO,CAAC;IACf,OAAO,EAAE,uCAAuC,oBAAoB,mBAAmB;CACxF,CAAC,CAAC"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a rate limit is stated on the wire, so a caller can pace itself instead
|
|
3
|
+
* of discovering the ceiling by hitting it.
|
|
4
|
+
*
|
|
5
|
+
* Two fields, from "RateLimit header fields for HTTP"
|
|
6
|
+
* (draft-ietf-httpapi-ratelimit-headers), and they answer different questions:
|
|
7
|
+
*
|
|
8
|
+
* - `RateLimit-Policy` is the STANDING allowance — the quota and the window
|
|
9
|
+
* the server allocates. It does not move between responses, so it can be
|
|
10
|
+
* stated on any response, including one that carried no credential. This is
|
|
11
|
+
* the field an agent reads once and paces against.
|
|
12
|
+
* - `RateLimit` is the LIVE position within that allowance — what is left and
|
|
13
|
+
* when it refills. It is per caller, so it appears only once a request has
|
|
14
|
+
* been attributed to a credential.
|
|
15
|
+
*
|
|
16
|
+
* Both are Structured Field Lists whose members are Strings naming the policy,
|
|
17
|
+
* carrying `q`/`w` (quota, window seconds) and `r`/`t` (remaining, seconds to
|
|
18
|
+
* reset) as parameters. Serializing them by hand is the reason this module
|
|
19
|
+
* exists: the field names and the parameter spellings were previously written
|
|
20
|
+
* out at each producer, so a change had to be made in every one of them and
|
|
21
|
+
* nothing failed when it was not.
|
|
22
|
+
*
|
|
23
|
+
* `Retry-After` is not defined here — it is plain HTTP (RFC 9110 § 10.2.3) and
|
|
24
|
+
* the value is a count of seconds. Its NAME lives here so a producer emitting
|
|
25
|
+
* the 429 triple names all three fields from one place.
|
|
26
|
+
*/
|
|
27
|
+
import { z } from 'zod';
|
|
28
|
+
/** The standing-allowance field. Safe on any response; independent of caller. */
|
|
29
|
+
export declare const RATE_LIMIT_POLICY_HEADER = "RateLimit-Policy";
|
|
30
|
+
/** The live-position field. Per caller, so only on an attributed request. */
|
|
31
|
+
export declare const RATE_LIMIT_HEADER = "RateLimit";
|
|
32
|
+
/** Seconds to wait before retrying, on a 429 or a 503 (RFC 9110 § 10.2.3). */
|
|
33
|
+
export declare const RETRY_AFTER_HEADER = "Retry-After";
|
|
34
|
+
/**
|
|
35
|
+
* One allocation of server capacity: how much, over how long.
|
|
36
|
+
*
|
|
37
|
+
* `quotaUnit` is omitted for the default unit, requests. A limiter that meters
|
|
38
|
+
* something else — bytes, operations — names it, and a client that does not
|
|
39
|
+
* recognize the unit knows to leave the number alone rather than read it as a
|
|
40
|
+
* request count.
|
|
41
|
+
*/
|
|
42
|
+
export declare const quotaPolicySchema: z.ZodObject<{
|
|
43
|
+
name: z.ZodString;
|
|
44
|
+
quota: z.ZodNumber;
|
|
45
|
+
windowSeconds: z.ZodOptional<z.ZodNumber>;
|
|
46
|
+
quotaUnit: z.ZodOptional<z.ZodString>;
|
|
47
|
+
}, z.core.$strip>;
|
|
48
|
+
export type QuotaPolicy = Readonly<z.infer<typeof quotaPolicySchema>>;
|
|
49
|
+
/** Where this caller currently stands against one {@link QuotaPolicy}. */
|
|
50
|
+
export declare const serviceLimitSchema: z.ZodObject<{
|
|
51
|
+
policy: z.ZodString;
|
|
52
|
+
remaining: z.ZodNumber;
|
|
53
|
+
resetSeconds: z.ZodOptional<z.ZodNumber>;
|
|
54
|
+
}, z.core.$strip>;
|
|
55
|
+
export type ServiceLimit = Readonly<z.infer<typeof serviceLimitSchema>>;
|
|
56
|
+
/** Serialize {@link QuotaPolicy} values into a `RateLimit-Policy` field value. */
|
|
57
|
+
export declare function rateLimitPolicyField(policies: readonly QuotaPolicy[]): string;
|
|
58
|
+
/** Serialize {@link ServiceLimit} values into a `RateLimit` field value. */
|
|
59
|
+
export declare function rateLimitField(limits: readonly ServiceLimit[]): string;
|
|
60
|
+
/** What a producer knows about the limit at the moment it writes the response. */
|
|
61
|
+
export declare const rateLimitSignalSchema: z.ZodObject<{
|
|
62
|
+
policies: z.ZodReadonly<z.ZodArray<z.ZodObject<{
|
|
63
|
+
name: z.ZodString;
|
|
64
|
+
quota: z.ZodNumber;
|
|
65
|
+
windowSeconds: z.ZodOptional<z.ZodNumber>;
|
|
66
|
+
quotaUnit: z.ZodOptional<z.ZodString>;
|
|
67
|
+
}, z.core.$strip>>>;
|
|
68
|
+
limits: z.ZodOptional<z.ZodReadonly<z.ZodArray<z.ZodObject<{
|
|
69
|
+
policy: z.ZodString;
|
|
70
|
+
remaining: z.ZodNumber;
|
|
71
|
+
resetSeconds: z.ZodOptional<z.ZodNumber>;
|
|
72
|
+
}, z.core.$strip>>>>;
|
|
73
|
+
retryAfterSeconds: z.ZodOptional<z.ZodNumber>;
|
|
74
|
+
}, z.core.$strip>;
|
|
75
|
+
export type RateLimitSignal = Readonly<z.infer<typeof rateLimitSignalSchema>>;
|
|
76
|
+
/**
|
|
77
|
+
* The header map for one response. This is the single call every producer
|
|
78
|
+
* makes, so the three fields cannot be emitted in one place and forgotten in
|
|
79
|
+
* the next.
|
|
80
|
+
*/
|
|
81
|
+
export declare function rateLimitHeaders(signal: RateLimitSignal): Record<string, string>;
|
|
82
|
+
//# sourceMappingURL=rateLimit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rateLimit.d.ts","sourceRoot":"","sources":["../../src/wire/rateLimit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,iFAAiF;AACjF,eAAO,MAAM,wBAAwB,qBAAqB,CAAC;AAE3D,6EAA6E;AAC7E,eAAO,MAAM,iBAAiB,cAAc,CAAC;AAE7C,8EAA8E;AAC9E,eAAO,MAAM,kBAAkB,gBAAgB,CAAC;AAWhD;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB;;;;;iBAS5B,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC,CAAC;AAEtE,0EAA0E;AAC1E,eAAO,MAAM,kBAAkB;;;;iBAO7B,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC,CAAC;AAoBxE,kFAAkF;AAClF,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,SAAS,WAAW,EAAE,GAAG,MAAM,CAa7E;AAED,4EAA4E;AAC5E,wBAAgB,cAAc,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,GAAG,MAAM,CAYtE;AAED,kFAAkF;AAClF,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;iBAOhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC,CAAC;AAE9E;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAWhF"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a rate limit is stated on the wire, so a caller can pace itself instead
|
|
3
|
+
* of discovering the ceiling by hitting it.
|
|
4
|
+
*
|
|
5
|
+
* Two fields, from "RateLimit header fields for HTTP"
|
|
6
|
+
* (draft-ietf-httpapi-ratelimit-headers), and they answer different questions:
|
|
7
|
+
*
|
|
8
|
+
* - `RateLimit-Policy` is the STANDING allowance — the quota and the window
|
|
9
|
+
* the server allocates. It does not move between responses, so it can be
|
|
10
|
+
* stated on any response, including one that carried no credential. This is
|
|
11
|
+
* the field an agent reads once and paces against.
|
|
12
|
+
* - `RateLimit` is the LIVE position within that allowance — what is left and
|
|
13
|
+
* when it refills. It is per caller, so it appears only once a request has
|
|
14
|
+
* been attributed to a credential.
|
|
15
|
+
*
|
|
16
|
+
* Both are Structured Field Lists whose members are Strings naming the policy,
|
|
17
|
+
* carrying `q`/`w` (quota, window seconds) and `r`/`t` (remaining, seconds to
|
|
18
|
+
* reset) as parameters. Serializing them by hand is the reason this module
|
|
19
|
+
* exists: the field names and the parameter spellings were previously written
|
|
20
|
+
* out at each producer, so a change had to be made in every one of them and
|
|
21
|
+
* nothing failed when it was not.
|
|
22
|
+
*
|
|
23
|
+
* `Retry-After` is not defined here — it is plain HTTP (RFC 9110 § 10.2.3) and
|
|
24
|
+
* the value is a count of seconds. Its NAME lives here so a producer emitting
|
|
25
|
+
* the 429 triple names all three fields from one place.
|
|
26
|
+
*/
|
|
27
|
+
import { z } from 'zod';
|
|
28
|
+
/** The standing-allowance field. Safe on any response; independent of caller. */
|
|
29
|
+
export const RATE_LIMIT_POLICY_HEADER = 'RateLimit-Policy';
|
|
30
|
+
/** The live-position field. Per caller, so only on an attributed request. */
|
|
31
|
+
export const RATE_LIMIT_HEADER = 'RateLimit';
|
|
32
|
+
/** Seconds to wait before retrying, on a 429 or a 503 (RFC 9110 § 10.2.3). */
|
|
33
|
+
export const RETRY_AFTER_HEADER = 'Retry-After';
|
|
34
|
+
/**
|
|
35
|
+
* A policy name as it appears inside the fields. Constrained to characters that
|
|
36
|
+
* need no Structured-Fields escaping, which is what lets the serializers below
|
|
37
|
+
* be a template rather than a String encoder — and what makes an unquotable
|
|
38
|
+
* name a thrown error at the definition site rather than a malformed header on
|
|
39
|
+
* the wire.
|
|
40
|
+
*/
|
|
41
|
+
const POLICY_NAME = /^[A-Za-z0-9_-]{1,64}$/;
|
|
42
|
+
/**
|
|
43
|
+
* One allocation of server capacity: how much, over how long.
|
|
44
|
+
*
|
|
45
|
+
* `quotaUnit` is omitted for the default unit, requests. A limiter that meters
|
|
46
|
+
* something else — bytes, operations — names it, and a client that does not
|
|
47
|
+
* recognize the unit knows to leave the number alone rather than read it as a
|
|
48
|
+
* request count.
|
|
49
|
+
*/
|
|
50
|
+
export const quotaPolicySchema = z.object({
|
|
51
|
+
/** Identifier the matching {@link ServiceLimit} refers back to. */
|
|
52
|
+
name: z.string(),
|
|
53
|
+
/** `q` — the allocation, in `quotaUnit`. */
|
|
54
|
+
quota: z.number(),
|
|
55
|
+
/** `w` — the window the allocation applies over, in whole seconds. */
|
|
56
|
+
windowSeconds: z.number().optional(),
|
|
57
|
+
/** `qu` — the unit `quota` counts. Omit for the default, requests. */
|
|
58
|
+
quotaUnit: z.string().optional(),
|
|
59
|
+
});
|
|
60
|
+
/** Where this caller currently stands against one {@link QuotaPolicy}. */
|
|
61
|
+
export const serviceLimitSchema = z.object({
|
|
62
|
+
/** The {@link QuotaPolicy} name this position is measured against. */
|
|
63
|
+
policy: z.string(),
|
|
64
|
+
/** `r` — units left in the current window. */
|
|
65
|
+
remaining: z.number(),
|
|
66
|
+
/** `t` — seconds until the allocation refills. */
|
|
67
|
+
resetSeconds: z.number().optional(),
|
|
68
|
+
});
|
|
69
|
+
function assertName(name, field) {
|
|
70
|
+
if (!POLICY_NAME.test(name)) {
|
|
71
|
+
throw new Error(`${field} policy name ${JSON.stringify(name)} must match ${String(POLICY_NAME)}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* A non-negative whole number, which is what both fields' numeric parameters
|
|
76
|
+
* are. A fractional or negative value is a producer bug; clamping it here keeps
|
|
77
|
+
* one malformed reading from making the whole field unparseable for the client.
|
|
78
|
+
*/
|
|
79
|
+
function integer(value) {
|
|
80
|
+
if (!Number.isFinite(value))
|
|
81
|
+
return 0;
|
|
82
|
+
return Math.max(0, Math.floor(value));
|
|
83
|
+
}
|
|
84
|
+
/** Serialize {@link QuotaPolicy} values into a `RateLimit-Policy` field value. */
|
|
85
|
+
export function rateLimitPolicyField(policies) {
|
|
86
|
+
if (policies.length === 0) {
|
|
87
|
+
throw new Error(`${RATE_LIMIT_POLICY_HEADER} must name at least one policy`);
|
|
88
|
+
}
|
|
89
|
+
return policies
|
|
90
|
+
.map((policy) => {
|
|
91
|
+
assertName(policy.name, RATE_LIMIT_POLICY_HEADER);
|
|
92
|
+
let item = `"${policy.name}";q=${integer(policy.quota)}`;
|
|
93
|
+
if (policy.quotaUnit !== undefined)
|
|
94
|
+
item += `;qu="${policy.quotaUnit}"`;
|
|
95
|
+
if (policy.windowSeconds !== undefined)
|
|
96
|
+
item += `;w=${integer(policy.windowSeconds)}`;
|
|
97
|
+
return item;
|
|
98
|
+
})
|
|
99
|
+
.join(', ');
|
|
100
|
+
}
|
|
101
|
+
/** Serialize {@link ServiceLimit} values into a `RateLimit` field value. */
|
|
102
|
+
export function rateLimitField(limits) {
|
|
103
|
+
if (limits.length === 0) {
|
|
104
|
+
throw new Error(`${RATE_LIMIT_HEADER} must name at least one policy`);
|
|
105
|
+
}
|
|
106
|
+
return limits
|
|
107
|
+
.map((limit) => {
|
|
108
|
+
assertName(limit.policy, RATE_LIMIT_HEADER);
|
|
109
|
+
let item = `"${limit.policy}";r=${integer(limit.remaining)}`;
|
|
110
|
+
if (limit.resetSeconds !== undefined)
|
|
111
|
+
item += `;t=${integer(limit.resetSeconds)}`;
|
|
112
|
+
return item;
|
|
113
|
+
})
|
|
114
|
+
.join(', ');
|
|
115
|
+
}
|
|
116
|
+
/** What a producer knows about the limit at the moment it writes the response. */
|
|
117
|
+
export const rateLimitSignalSchema = z.object({
|
|
118
|
+
/** The standing allowance. Always known; stated on every response. */
|
|
119
|
+
policies: z.array(quotaPolicySchema).readonly(),
|
|
120
|
+
/** This caller's position, once the request has been attributed to one. */
|
|
121
|
+
limits: z.array(serviceLimitSchema).readonly().optional(),
|
|
122
|
+
/** Present only on a rejection, and only when a wait is what resolves it. */
|
|
123
|
+
retryAfterSeconds: z.number().optional(),
|
|
124
|
+
});
|
|
125
|
+
/**
|
|
126
|
+
* The header map for one response. This is the single call every producer
|
|
127
|
+
* makes, so the three fields cannot be emitted in one place and forgotten in
|
|
128
|
+
* the next.
|
|
129
|
+
*/
|
|
130
|
+
export function rateLimitHeaders(signal) {
|
|
131
|
+
const headers = {
|
|
132
|
+
[RATE_LIMIT_POLICY_HEADER]: rateLimitPolicyField(signal.policies),
|
|
133
|
+
};
|
|
134
|
+
if (signal.limits && signal.limits.length > 0) {
|
|
135
|
+
headers[RATE_LIMIT_HEADER] = rateLimitField(signal.limits);
|
|
136
|
+
}
|
|
137
|
+
if (signal.retryAfterSeconds !== undefined) {
|
|
138
|
+
headers[RETRY_AFTER_HEADER] = String(Math.max(1, integer(signal.retryAfterSeconds)));
|
|
139
|
+
}
|
|
140
|
+
return headers;
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=rateLimit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rateLimit.js","sourceRoot":"","sources":["../../src/wire/rateLimit.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abloatai/transaction",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.57.0",
|
|
4
4
|
"description": "The headless Ablo transaction client and canonical contracts for reads, commits, confirmation, claims, and durable observation.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The hosted Ablo
|
|
2
|
+
* The hosted Ablo hosts, with no dependencies of their own.
|
|
3
3
|
*
|
|
4
|
-
* This is the single place
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* This is the single place each published Ablo domain is declared: the API the
|
|
5
|
+
* SDK calls, the documentation site every error link and descriptor points at,
|
|
6
|
+
* and the marketing site that carries signup and the legal pages. URL
|
|
7
|
+
* resolution, the CLI's default URL, the data-source connector's base, the
|
|
8
|
+
* generated OpenAPI server entry, the error registry's `doc_url`, and the
|
|
9
|
+
* published discovery descriptors all import from here, so changing a domain is
|
|
10
|
+
* a one-line edit.
|
|
8
11
|
*
|
|
9
12
|
* These constants are kept dependency-free deliberately: several low-level
|
|
10
13
|
* modules consume them, and routing those modules through the auth layer would
|
|
@@ -22,3 +25,18 @@ export const ABLO_HOSTED_HTTP_BASE_URL = `https://${ABLO_HOSTED_API_DOMAIN}`;
|
|
|
22
25
|
* {@link ABLO_HOSTED_HTTP_BASE_URL}; kept as a distinct name because it is
|
|
23
26
|
* the documented client-options default. */
|
|
24
27
|
export const ABLO_DEFAULT_BASE_URL = ABLO_HOSTED_HTTP_BASE_URL;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The documentation site. Canonical target of every error `doc_url`, of the
|
|
31
|
+
* published OpenAPI document, and of the discovery descriptors under
|
|
32
|
+
* `/.well-known`, which is why it is declared beside the API host rather than
|
|
33
|
+
* retyped at each of those call sites.
|
|
34
|
+
*/
|
|
35
|
+
export const ABLO_DOCS_BASE_URL = 'https://docs.abloatai.com';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The marketing site: signup, the CLI device-flow approval page, and the legal
|
|
39
|
+
* pages a machine-readable onboarding descriptor has to cite. The apex
|
|
40
|
+
* redirects here, so this is the form to publish.
|
|
41
|
+
*/
|
|
42
|
+
export const ABLO_SITE_BASE_URL = 'https://www.abloatai.com';
|
package/src/auth/index.ts
CHANGED
|
@@ -26,8 +26,10 @@ import type {
|
|
|
26
26
|
} from './capability.js';
|
|
27
27
|
export {
|
|
28
28
|
ABLO_DEFAULT_BASE_URL,
|
|
29
|
+
ABLO_DOCS_BASE_URL,
|
|
29
30
|
ABLO_HOSTED_API_DOMAIN,
|
|
30
31
|
ABLO_HOSTED_HTTP_BASE_URL,
|
|
32
|
+
ABLO_SITE_BASE_URL,
|
|
31
33
|
} from './hostedEndpoints.js';
|
|
32
34
|
export { normalizeAbloBaseUrl } from './baseUrl.js';
|
|
33
35
|
|
|
@@ -23,7 +23,6 @@ export {
|
|
|
23
23
|
// Layer 3 — optimistic stale-context
|
|
24
24
|
onStaleModeSchema,
|
|
25
25
|
MAX_READ_SET_ENTRIES,
|
|
26
|
-
readSetWatermarkSchema,
|
|
27
26
|
readSetRowTargetSchema,
|
|
28
27
|
readSetGroupTargetSchema,
|
|
29
28
|
commitReadSetTargetSchema,
|
|
@@ -117,7 +116,6 @@ export type {
|
|
|
117
116
|
TargetRef,
|
|
118
117
|
// Layer 3 — optimistic stale-context
|
|
119
118
|
OnStaleMode,
|
|
120
|
-
ReadSetWatermark,
|
|
121
119
|
ReadSetRowTarget,
|
|
122
120
|
ReadSetGroupTarget,
|
|
123
121
|
CommitReadSetTarget,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { logPositionSchema, type LogPosition } from '../syncLog/contract.js';
|
|
2
3
|
import {
|
|
3
4
|
syncGroupInputSchema,
|
|
4
5
|
syncGroupRefSchema,
|
|
@@ -267,10 +268,6 @@ const streamTargetSchema = targetRefSchema
|
|
|
267
268
|
export const onStaleModeSchema = z.enum(['reject', 'overwrite', 'notify']);
|
|
268
269
|
export type OnStaleMode = z.infer<typeof onStaleModeSchema>;
|
|
269
270
|
|
|
270
|
-
/** The exact authoritative position retained for one ReadSet entry. */
|
|
271
|
-
export const readSetWatermarkSchema = z.number().int().nonnegative();
|
|
272
|
-
export type ReadSetWatermark = z.infer<typeof readSetWatermarkSchema>;
|
|
273
|
-
|
|
274
271
|
/** Maximum decision-input entries one logical commit may ask the server to scan. */
|
|
275
272
|
export const MAX_READ_SET_ENTRIES = 500;
|
|
276
273
|
|
|
@@ -285,7 +282,7 @@ export type TrackOnStale = z.infer<typeof trackOnStaleSchema>;
|
|
|
285
282
|
* claim — see the claim layer below.
|
|
286
283
|
*/
|
|
287
284
|
export const writeGuardSchema = z.object({
|
|
288
|
-
readAt:
|
|
285
|
+
readAt: logPositionSchema.nullish(),
|
|
289
286
|
onStale: onStaleModeSchema.nullish(),
|
|
290
287
|
bypass: z.boolean().optional(),
|
|
291
288
|
});
|
|
@@ -489,7 +486,7 @@ export type PersistedReadSetTarget = z.infer<typeof persistedReadSetTargetSchema
|
|
|
489
486
|
/** One exact input to the decision made by this commit. */
|
|
490
487
|
export const commitReadSetEntrySchema = z.object({
|
|
491
488
|
target: commitReadSetTargetSchema,
|
|
492
|
-
watermark:
|
|
489
|
+
watermark: logPositionSchema,
|
|
493
490
|
lifetime: z.literal('commit'),
|
|
494
491
|
onStale: onStaleModeSchema,
|
|
495
492
|
});
|
|
@@ -498,7 +495,7 @@ export type CommitReadSetEntry = z.infer<typeof commitReadSetEntrySchema>;
|
|
|
498
495
|
/** One exact input retained after the declaring commit settles. */
|
|
499
496
|
export const persistedReadSetEntrySchema = z.object({
|
|
500
497
|
target: persistedReadSetTargetSchema,
|
|
501
|
-
watermark:
|
|
498
|
+
watermark: logPositionSchema,
|
|
502
499
|
lifetime: z.literal('persisted'),
|
|
503
500
|
onStale: trackOnStaleSchema,
|
|
504
501
|
});
|
package/src/errorCodes.ts
CHANGED
|
@@ -846,6 +846,12 @@ export const ERROR_CODES = {
|
|
|
846
846
|
false,
|
|
847
847
|
'The database rejected the write on a foreign-key constraint: a referenced row does not exist, or the row being deleted is still referenced by others. The error details name the constraint.'
|
|
848
848
|
),
|
|
849
|
+
entity_already_exists: wire(
|
|
850
|
+
'conflict',
|
|
851
|
+
409,
|
|
852
|
+
false,
|
|
853
|
+
'A row already exists with this id. CREATE is strict; use UPDATE for an existing row.'
|
|
854
|
+
),
|
|
849
855
|
unique_violation: wire(
|
|
850
856
|
'conflict',
|
|
851
857
|
409,
|
package/src/errors.ts
CHANGED
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
|
|
18
18
|
import { z } from 'zod';
|
|
19
19
|
import type { ErrorCode } from './errorCodes.js';
|
|
20
|
+
// Dependency-free by design, so the error registry can name the docs host
|
|
21
|
+
// without pulling the auth layer into this module's import graph.
|
|
22
|
+
import { ABLO_DOCS_BASE_URL } from './auth/hostedEndpoints.js';
|
|
20
23
|
import { errorCodeSpec, classifyRecovery } from './errorCodes.js';
|
|
21
24
|
import {
|
|
22
25
|
wireClaimSummarySchema,
|
|
@@ -156,7 +159,7 @@ export class AbloError extends Error {
|
|
|
156
159
|
* a `doc_url` automatically.
|
|
157
160
|
*/
|
|
158
161
|
export function docUrlForCode(code: ErrorCode): string {
|
|
159
|
-
return
|
|
162
|
+
return `${ABLO_DOCS_BASE_URL}/errors#${code}`;
|
|
160
163
|
}
|
|
161
164
|
|
|
162
165
|
/** 401 — invalid/missing/expired credentials. */
|
package/src/readSetContext.ts
CHANGED
|
@@ -97,9 +97,15 @@ export function publishCommitRecord(
|
|
|
97
97
|
registry.commitRecords.set(record.id, record);
|
|
98
98
|
try {
|
|
99
99
|
const observed = registry.onCommitRecord?.(record);
|
|
100
|
-
if (observed)
|
|
101
|
-
|
|
100
|
+
if (observed) {
|
|
101
|
+
void Promise.resolve(observed).catch((error) => {
|
|
102
|
+
// The observer is outside the transaction outcome by contract.
|
|
103
|
+
void error;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
} catch (error) {
|
|
102
107
|
// Observability must never change a commit's outcome.
|
|
108
|
+
void error;
|
|
103
109
|
}
|
|
104
110
|
}
|
|
105
111
|
|
|
@@ -19,9 +19,12 @@ import type {
|
|
|
19
19
|
CommitRecordList,
|
|
20
20
|
CommitRecordListOptions,
|
|
21
21
|
CommitRecordWhere,
|
|
22
|
+
CommitOperationBody,
|
|
23
|
+
ModelOperationAction,
|
|
22
24
|
} from '../wire/commit.js';
|
|
23
25
|
import type { LogListResponse, LogQuery } from '../wire/feedEvent.js';
|
|
24
26
|
import type { ModelListEvidence } from '../wire/modelResponses.js';
|
|
27
|
+
import { AbloValidationError } from '../errors.js';
|
|
25
28
|
// Re-exported, not redeclared. `wire/commit.ts` owns the commit-status vocabulary
|
|
26
29
|
// and derives the waitable subset from it; this module serves that name to SDK
|
|
27
30
|
// consumers. Restating the subset here as its own union produced a type that
|
|
@@ -63,6 +66,7 @@ import type {
|
|
|
63
66
|
AwaitedClaimMethod,
|
|
64
67
|
ModelTrackParams,
|
|
65
68
|
ModelTrackResult,
|
|
69
|
+
ModelCreateManyParams,
|
|
66
70
|
ServerReadOptions,
|
|
67
71
|
} from './modelOperations.js';
|
|
68
72
|
|
|
@@ -77,12 +81,7 @@ import type {
|
|
|
77
81
|
* `claim({ id })` — a durable claim handle for coordinated writes
|
|
78
82
|
*/
|
|
79
83
|
|
|
80
|
-
export type ModelOperationAction
|
|
81
|
-
| 'create'
|
|
82
|
-
| 'update'
|
|
83
|
-
| 'delete'
|
|
84
|
-
| 'archive'
|
|
85
|
-
| 'unarchive';
|
|
84
|
+
export type { ModelOperationAction };
|
|
86
85
|
|
|
87
86
|
|
|
88
87
|
/** @internal Transport envelope; the public typed client returns the row. */
|
|
@@ -130,20 +129,119 @@ export interface HttpTransportList<T = Record<string, unknown>> {
|
|
|
130
129
|
* and a complete one the same value, so the caller with 500 matching rows got
|
|
131
130
|
* 20 and no way to find out.
|
|
132
131
|
*/
|
|
133
|
-
export type ModelList<T> = T[] & Pick<HttpTransportList<T>, 'hasMore' | 'nextCursor'
|
|
132
|
+
export type ModelList<T> = T[] & Pick<HttpTransportList<T>, 'hasMore' | 'nextCursor'> &
|
|
133
|
+
AsyncIterable<T>;
|
|
134
134
|
|
|
135
135
|
/**
|
|
136
|
-
*
|
|
137
|
-
*
|
|
136
|
+
* How far a `for await` over a list will walk before it gives up.
|
|
137
|
+
*
|
|
138
|
+
* A cursor that stops advancing would otherwise spin forever. The bound is
|
|
139
|
+
* high enough that no real collection reaches it and low enough that a broken
|
|
140
|
+
* server is a failed read rather than a hung process.
|
|
141
|
+
*/
|
|
142
|
+
const AUTO_PAGE_LIMIT = 10_000;
|
|
143
|
+
const LIST_ALL_PAGE_LIMIT = 100;
|
|
144
|
+
|
|
145
|
+
const nextPageFor = new WeakMap<object, (cursor: string) => Promise<ModelList<unknown>>>();
|
|
146
|
+
|
|
147
|
+
export interface ModelListWalkOptions {
|
|
148
|
+
readonly maxPages?: number;
|
|
149
|
+
readonly signal?: AbortSignal;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function* walkModelList<T>(
|
|
153
|
+
first: ModelList<T>,
|
|
154
|
+
options: ModelListWalkOptions = {},
|
|
155
|
+
): AsyncGenerator<T> {
|
|
156
|
+
const maxPages = options.maxPages ?? AUTO_PAGE_LIMIT;
|
|
157
|
+
if (!Number.isInteger(maxPages) || maxPages < 1) {
|
|
158
|
+
throw new AbloValidationError('maxPages must be a positive integer.', {
|
|
159
|
+
code: 'invalid_options',
|
|
160
|
+
param: 'maxPages',
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let current = first;
|
|
165
|
+
for (let visited = 0; visited < maxPages; visited += 1) {
|
|
166
|
+
options.signal?.throwIfAborted();
|
|
167
|
+
for (let i = 0; i < current.length; i += 1) {
|
|
168
|
+
options.signal?.throwIfAborted();
|
|
169
|
+
yield current[i] as T;
|
|
170
|
+
}
|
|
171
|
+
const cursor = current.nextCursor;
|
|
172
|
+
const fetchNext = nextPageFor.get(current) as
|
|
173
|
+
| ((nextCursor: string) => Promise<ModelList<T>>)
|
|
174
|
+
| undefined;
|
|
175
|
+
if (!current.hasMore || cursor === null || fetchNext === undefined) return;
|
|
176
|
+
const next = await fetchNext(cursor);
|
|
177
|
+
if (next.nextCursor === cursor) {
|
|
178
|
+
throw new AbloValidationError(
|
|
179
|
+
`Walking this list received the same continuation cursor twice (${JSON.stringify(cursor)}). ` +
|
|
180
|
+
'The collection may be incomplete, so traversal stopped with an error.',
|
|
181
|
+
{ code: 'malformed_response', param: 'nextCursor' },
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
current = next;
|
|
185
|
+
}
|
|
186
|
+
throw new AbloValidationError(
|
|
187
|
+
`Walking this list passed ${maxPages} pages without reaching the end. ` +
|
|
188
|
+
`Narrow the read with \`where\` or raise \`maxPages\` deliberately.`,
|
|
189
|
+
{ code: 'invalid_options', param: 'maxPages' },
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Collect a complete list through the same guarded cursor loop as async iteration. */
|
|
194
|
+
export async function collectModelList<T>(
|
|
195
|
+
first: ModelList<T>,
|
|
196
|
+
options: ModelListWalkOptions = {},
|
|
197
|
+
): Promise<T[]> {
|
|
198
|
+
const rows: T[] = [];
|
|
199
|
+
for await (const row of walkModelList(first, {
|
|
200
|
+
maxPages: options.maxPages ?? LIST_ALL_PAGE_LIMIT,
|
|
201
|
+
signal: options.signal,
|
|
202
|
+
})) rows.push(row);
|
|
203
|
+
return rows;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Attach the page state to the rows, and make the list walk its own pages.
|
|
208
|
+
*
|
|
209
|
+
* `hasMore` and `nextCursor` are non-enumerable, so the result stays
|
|
210
|
+
* indistinguishable from a plain array everywhere they are not read: it maps,
|
|
211
|
+
* filters, spreads, and `JSON.stringify`s exactly as the rows always did.
|
|
212
|
+
*
|
|
213
|
+
* The async iterator is the answer to the question that shape raises. A list
|
|
214
|
+
* read is a page — the server applies a default size and caps the largest —
|
|
215
|
+
* and a page of 20 looks precisely like a complete answer of 20, so every
|
|
216
|
+
* caller either checked `hasMore` or, far more often, reasoned about a
|
|
217
|
+
* truncated set without knowing it. Hand-rolled page walkers were the common
|
|
218
|
+
* result, and each one re-derived the same cursor loop and the same
|
|
219
|
+
* non-advancing-cursor guard.
|
|
220
|
+
*
|
|
221
|
+
* So iterate the value to get the page, and `for await` it to get the
|
|
222
|
+
* collection:
|
|
223
|
+
*
|
|
224
|
+
* ```ts
|
|
225
|
+
* const page = await ablo.issue.list({ where: { teamId } });
|
|
226
|
+
* for (const issue of page) … // the 20 rows that came back
|
|
227
|
+
* for await (const issue of page) … // every issue, paged as it goes
|
|
228
|
+
* ```
|
|
138
229
|
*/
|
|
139
230
|
export function modelList<T>(
|
|
140
231
|
rows: readonly T[],
|
|
141
232
|
page: Pick<HttpTransportList<unknown>, 'hasMore' | 'nextCursor'>,
|
|
233
|
+
/** Reads the page after `cursor`. Omitted where no transport can follow. */
|
|
234
|
+
fetchNext?: (cursor: string) => Promise<ModelList<T>>,
|
|
142
235
|
): ModelList<T> {
|
|
143
|
-
|
|
236
|
+
const list = Object.defineProperties([...rows], {
|
|
144
237
|
hasMore: { value: page.hasMore, enumerable: false },
|
|
145
238
|
nextCursor: { value: page.nextCursor, enumerable: false },
|
|
239
|
+
[Symbol.asyncIterator]: { value: () => walkModelList(list), enumerable: false },
|
|
146
240
|
}) as ModelList<T>;
|
|
241
|
+
if (fetchNext) {
|
|
242
|
+
nextPageFor.set(list, fetchNext as (cursor: string) => Promise<ModelList<unknown>>);
|
|
243
|
+
}
|
|
244
|
+
return list;
|
|
147
245
|
}
|
|
148
246
|
|
|
149
247
|
export type IfClaimedPolicy = 'return' | 'fail';
|
|
@@ -206,21 +304,8 @@ export interface ClaimCreateOptions {
|
|
|
206
304
|
readonly onStatus?: (event: ClaimAttemptEvent) => void;
|
|
207
305
|
}
|
|
208
306
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
/** The model name — matches `ablo.<model>` and the schema's `model()`. */
|
|
212
|
-
readonly model: string;
|
|
213
|
-
readonly id?: string | null;
|
|
214
|
-
readonly data?: Record<string, unknown> | null;
|
|
215
|
-
readonly where?: Record<string, unknown> | null;
|
|
216
|
-
readonly transactionId?: string | null;
|
|
217
|
-
/** Claim identity derived from a held claim; not an application id. */
|
|
218
|
-
readonly claimId?: string | null;
|
|
219
|
-
readonly readAt?: number | null;
|
|
220
|
-
readonly onStale?: OnStaleMode | null;
|
|
221
|
-
/** Fencing token (Option B) from the batch's claim handle; server-validated. */
|
|
222
|
-
readonly fenceToken?: number | null;
|
|
223
|
-
}
|
|
307
|
+
/** Public commit operation inferred from the canonical request-body schema. */
|
|
308
|
+
export type CommitOperationInput = CommitOperationBody;
|
|
224
309
|
|
|
225
310
|
export interface CommitCreateOptions {
|
|
226
311
|
readonly idempotencyKey?: string | null;
|
|
@@ -402,6 +487,12 @@ export interface HttpTransportModel<
|
|
|
402
487
|
* returned, not the input.
|
|
403
488
|
*/
|
|
404
489
|
create(params: ModelMutationOptions & { readonly data: Record<string, unknown>; readonly id?: string | null }): Promise<T>;
|
|
490
|
+
/**
|
|
491
|
+
* Creates many rows as one atomic commit and returns them, in the caller's
|
|
492
|
+
* order. One rejected row declines the batch. The rows are the server's own,
|
|
493
|
+
* carried back on the commit rather than read again afterwards.
|
|
494
|
+
*/
|
|
495
|
+
createMany(params: ModelCreateManyParams<Record<string, unknown>>): Promise<T[]>;
|
|
405
496
|
update(params: ModelMutationOptions & { readonly id: string; readonly data: Record<string, unknown> }): Promise<CommitReceipt>;
|
|
406
497
|
/**
|
|
407
498
|
* Update under contention with a function of the latest state —
|