@allsourcedev/client 0.23.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 +161 -0
- package/dist/circuit-breaker.d.ts +19 -0
- package/dist/circuit-breaker.d.ts.map +1 -0
- package/dist/client.d.ts +48 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/fold.d.ts +11 -0
- package/dist/fold.d.ts.map +1 -0
- package/dist/index.cjs +297 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +265 -0
- package/dist/types.d.ts +152 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# @allsourcedev/client
|
|
2
|
+
|
|
3
|
+
TypeScript/JavaScript client for the [AllSource](https://all-source.xyz) event store API.
|
|
4
|
+
|
|
5
|
+
Talks to the AllSource **Query Service** (the public gateway), not Core directly.
|
|
6
|
+
Ships compiled ESM + CJS with `.d.ts` types. Runs on Node 18+ and Bun. Zero
|
|
7
|
+
runtime dependencies (uses the global `fetch`).
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @allsourcedev/client
|
|
13
|
+
# or
|
|
14
|
+
bun add @allsourcedev/client
|
|
15
|
+
# or
|
|
16
|
+
pnpm add @allsourcedev/client
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
> **Why not a git install?** `@allsourcedev/client` lives in the `all-source`
|
|
20
|
+
> monorepo under `sdks/typescript`. Git installers can't pull a sub-directory of
|
|
21
|
+
> a monorepo — that's why `bun add github:…#workspace=@allsourcedev/client` and
|
|
22
|
+
> `#path:sdks/typescript` return 404. Install from the npm registry (above).
|
|
23
|
+
|
|
24
|
+
## Authentication
|
|
25
|
+
|
|
26
|
+
AllSource uses API keys (signed JWTs). Get one from your dashboard at
|
|
27
|
+
[all-source.xyz](https://all-source.xyz) → **Settings → API Keys**. The key is
|
|
28
|
+
sent in the `X-API-Key` header; the SDK does this for you when you pass `apiKey`
|
|
29
|
+
to the constructor.
|
|
30
|
+
|
|
31
|
+
Store the key in an env var rather than hard-coding it:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { AllSourceClient } from "@allsourcedev/client";
|
|
35
|
+
|
|
36
|
+
const client = new AllSourceClient({
|
|
37
|
+
baseUrl: "https://allsource-query.fly.dev", // your Query Service URL
|
|
38
|
+
apiKey: process.env.ALLSOURCE_API_KEY!,
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The tenant is taken from the key — there is no separate tenant argument.
|
|
43
|
+
|
|
44
|
+
## Quick Start
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { AllSourceClient } from "@allsourcedev/client";
|
|
48
|
+
|
|
49
|
+
const client = new AllSourceClient({
|
|
50
|
+
baseUrl: "https://allsource-query.fly.dev",
|
|
51
|
+
apiKey: process.env.ALLSOURCE_API_KEY!,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Ingest an event — returns the stored event
|
|
55
|
+
const event = await client.ingestEvent({
|
|
56
|
+
event_type: "user.signup",
|
|
57
|
+
entity_id: "user-abc-123",
|
|
58
|
+
payload: { email: "jane@example.com", plan: "pro" },
|
|
59
|
+
metadata: { source: "web", ip: "1.2.3.4" },
|
|
60
|
+
});
|
|
61
|
+
console.log("Ingested:", event.id);
|
|
62
|
+
|
|
63
|
+
// Query events back
|
|
64
|
+
const { events, count } = await client.queryEvents({
|
|
65
|
+
entity_id: "user-abc-123",
|
|
66
|
+
limit: 50,
|
|
67
|
+
});
|
|
68
|
+
console.log(`Found ${count} events`);
|
|
69
|
+
|
|
70
|
+
// Health check (unauthenticated)
|
|
71
|
+
const health = await client.getHealth();
|
|
72
|
+
console.log("Status:", health.status);
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## API
|
|
76
|
+
|
|
77
|
+
### `new AllSourceClient(config)`
|
|
78
|
+
|
|
79
|
+
| Option | Type | Required | Default | Description |
|
|
80
|
+
|------------------|------------|----------|---------|--------------------------------------|
|
|
81
|
+
| `baseUrl` | `string` | Yes | — | AllSource Query Service URL |
|
|
82
|
+
| `apiKey` | `string` | Yes | — | API key (sent as `X-API-Key`) |
|
|
83
|
+
| `timeout` | `number` | No | `30000` | Request timeout in milliseconds |
|
|
84
|
+
| `retry` | `object` | No | — | Retry/backoff overrides |
|
|
85
|
+
| `circuitBreaker` | `object` | No | — | Circuit-breaker overrides |
|
|
86
|
+
| `fetch` | `function` | No | global | Custom `fetch` implementation |
|
|
87
|
+
|
|
88
|
+
### `client.ingestEvent(event)` → `Event`
|
|
89
|
+
|
|
90
|
+
Ingest a single event and return the stored event (`id`, `timestamp`, …).
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
const stored = await client.ingestEvent({
|
|
94
|
+
event_type: "order.placed",
|
|
95
|
+
entity_id: "order-456",
|
|
96
|
+
payload: { total: 99.99, currency: "USD" },
|
|
97
|
+
metadata: { source: "checkout" }, // optional, preserved
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### `client.ingestBatch(events)` → `{ count, events }`
|
|
102
|
+
|
|
103
|
+
Ingest many events in one request.
|
|
104
|
+
|
|
105
|
+
### `client.queryEvents(params?)` → `{ events, count }`
|
|
106
|
+
|
|
107
|
+
Query events with optional filters.
|
|
108
|
+
|
|
109
|
+
| Param | Type | Description |
|
|
110
|
+
|--------------|----------|-----------------------------------|
|
|
111
|
+
| `entity_id` | `string` | Filter by entity ID |
|
|
112
|
+
| `event_type` | `string` | Filter by event type |
|
|
113
|
+
| `limit` | `number` | Max events to return |
|
|
114
|
+
| `offset` | `number` | Number of events to skip |
|
|
115
|
+
| `since` | `string` | Start time, inclusive (ISO 8601) |
|
|
116
|
+
| `until` | `string` | End time, inclusive (ISO 8601) |
|
|
117
|
+
|
|
118
|
+
### `client.queryAndFold(params, folder)` → `S | undefined`
|
|
119
|
+
|
|
120
|
+
Query events and fold them into a derived state with an `EventFolder<S>`.
|
|
121
|
+
|
|
122
|
+
### `client.listProjections()` → `{ projections, total }`
|
|
123
|
+
|
|
124
|
+
List projections from Core.
|
|
125
|
+
|
|
126
|
+
### Prime projections
|
|
127
|
+
|
|
128
|
+
`definePrimeProjection(entityType, fieldPolicies)`,
|
|
129
|
+
`projectNode(nodeId)`, `listPrimeProjections()`,
|
|
130
|
+
`nodeFieldProvenance(nodeId, field)` — declarative projections with per-field
|
|
131
|
+
merge policies and provenance.
|
|
132
|
+
|
|
133
|
+
### `client.getHealth()` → `HealthResponse`
|
|
134
|
+
|
|
135
|
+
Service health. Unauthenticated.
|
|
136
|
+
|
|
137
|
+
### Reliability
|
|
138
|
+
|
|
139
|
+
Every request goes through exponential-backoff retries (on 408/429/5xx and
|
|
140
|
+
network errors) and a circuit breaker. Both are configurable via the constructor.
|
|
141
|
+
|
|
142
|
+
### Error handling
|
|
143
|
+
|
|
144
|
+
API errors throw `AllSourceError` with `status` and `body`:
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
import { AllSourceClient, AllSourceError } from "@allsourcedev/client";
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
await client.ingestEvent({ /* … */ });
|
|
151
|
+
} catch (err) {
|
|
152
|
+
if (err instanceof AllSourceError) {
|
|
153
|
+
console.error(`API error ${err.status}:`, err.body);
|
|
154
|
+
if (err.isUnauthorized()) console.error("Check your API key.");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## License
|
|
160
|
+
|
|
161
|
+
MIT
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type CircuitBreakerConfig } from "./types";
|
|
2
|
+
export type CircuitState = "closed" | "open" | "half-open";
|
|
3
|
+
export declare class CircuitBreaker {
|
|
4
|
+
private readonly threshold;
|
|
5
|
+
private readonly recoveryTimeout;
|
|
6
|
+
private consecutiveFailures;
|
|
7
|
+
private lastFailureTime;
|
|
8
|
+
private currentState;
|
|
9
|
+
constructor(config?: Partial<CircuitBreakerConfig>);
|
|
10
|
+
/** Returns the current circuit state. */
|
|
11
|
+
get state(): CircuitState;
|
|
12
|
+
/** Check if a request is allowed. Throws CircuitOpenError if the circuit is open. */
|
|
13
|
+
check(): void;
|
|
14
|
+
/** Record a successful request. Resets the circuit to closed. */
|
|
15
|
+
recordSuccess(): void;
|
|
16
|
+
/** Record a failed request. Opens the circuit after threshold consecutive failures. */
|
|
17
|
+
recordFailure(): void;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=circuit-breaker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"circuit-breaker.d.ts","sourceRoot":"","sources":["../src/circuit-breaker.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,oBAAoB,EAAoB,MAAM,SAAS,CAAC;AAEtE,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,MAAM,GAAG,WAAW,CAAC;AAO3D,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,mBAAmB,CAAK;IAChC,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,YAAY,CAA0B;gBAElC,MAAM,GAAE,OAAO,CAAC,oBAAoB,CAAM;IAKtD,yCAAyC;IACzC,IAAI,KAAK,IAAI,YAAY,CAQxB;IAED,qFAAqF;IACrF,KAAK,IAAI,IAAI;IAQb,iEAAiE;IACjE,aAAa,IAAI,IAAI;IAKrB,uFAAuF;IACvF,aAAa,IAAI,IAAI;CAOtB"}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type EventFolder } from "./fold";
|
|
2
|
+
import { type AllSourceConfig, type CreatedEvent, type HealthResponse, type IngestEventInput, type PrimeProjection, type PrimeProjectionAck, type PrimeProvenance, type PrimeSnapshot, type ProjectionsResponse, type QueryEventsParams, type QueryEventsResponse } from "./types";
|
|
3
|
+
export declare class AllSourceClient {
|
|
4
|
+
private readonly baseUrl;
|
|
5
|
+
private readonly apiKey;
|
|
6
|
+
private readonly timeout;
|
|
7
|
+
private readonly retryConfig;
|
|
8
|
+
private readonly circuitBreaker;
|
|
9
|
+
private readonly fetch;
|
|
10
|
+
constructor(config: AllSourceConfig);
|
|
11
|
+
/**
|
|
12
|
+
* Ingest a single event into AllSource Core.
|
|
13
|
+
*
|
|
14
|
+
* Returns the created event's `id`, `timestamp` and `version`. The gateway
|
|
15
|
+
* wraps the ack in a `{ data }` envelope and keys the id as `event_id`; this
|
|
16
|
+
* unwraps and normalizes it to `id`.
|
|
17
|
+
*/
|
|
18
|
+
ingestEvent(event: IngestEventInput): Promise<CreatedEvent>;
|
|
19
|
+
/**
|
|
20
|
+
* Ingest a batch of events into AllSource Core.
|
|
21
|
+
*
|
|
22
|
+
* Returns how many were ingested and the created events
|
|
23
|
+
* (`id` / `timestamp` / `version`).
|
|
24
|
+
*/
|
|
25
|
+
ingestBatch(events: IngestEventInput[]): Promise<{
|
|
26
|
+
count: number;
|
|
27
|
+
events: CreatedEvent[];
|
|
28
|
+
}>;
|
|
29
|
+
/** Query events with optional filters. */
|
|
30
|
+
queryEvents(params?: QueryEventsParams): Promise<QueryEventsResponse>;
|
|
31
|
+
/** List all projections from AllSource Core. */
|
|
32
|
+
listProjections(): Promise<ProjectionsResponse>;
|
|
33
|
+
/** List all Prime projection definitions from the gateway. */
|
|
34
|
+
listPrimeProjections(): Promise<PrimeProjection[]>;
|
|
35
|
+
/** Define (or update) a Prime projection with per-field merge policies. */
|
|
36
|
+
definePrimeProjection(entityType: string, fieldPolicies: Record<string, string>): Promise<PrimeProjectionAck>;
|
|
37
|
+
/** Project a Prime node into a materialized snapshot. */
|
|
38
|
+
projectNode(nodeId: string): Promise<PrimeSnapshot>;
|
|
39
|
+
/** Fetch provenance for a single field on a Prime node. Throws AllSourceError (404) when none. */
|
|
40
|
+
nodeFieldProvenance(nodeId: string, field: string): Promise<PrimeProvenance>;
|
|
41
|
+
/** Query events and fold them into a state using the provided folder. */
|
|
42
|
+
queryAndFold<S>(params: QueryEventsParams, folder: EventFolder<S>): Promise<S | undefined>;
|
|
43
|
+
/** Check the health of the AllSource service. */
|
|
44
|
+
getHealth(): Promise<HealthResponse>;
|
|
45
|
+
private request;
|
|
46
|
+
private computeDelay;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,WAAW,EAAc,MAAM,QAAQ,CAAC;AACtD,OAAO,EACL,KAAK,eAAe,EAEpB,KAAK,YAAY,EAEjB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EAEzB,MAAM,SAAS,CAAC;AA6BjB,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAc;IAC1C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAiB;IAChD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA0B;gBAEpC,MAAM,EAAE,eAAe;IAmBnC;;;;;;OAMG;IACG,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,YAAY,CAAC;IASjE;;;;;OAKG;IACG,WAAW,CACf,MAAM,EAAE,gBAAgB,EAAE,GACzB,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,YAAY,EAAE,CAAA;KAAE,CAAC;IASrD,0CAA0C;IACpC,WAAW,CACf,MAAM,GAAE,iBAAsB,GAC7B,OAAO,CAAC,mBAAmB,CAAC;IAc/B,gDAAgD;IAC1C,eAAe,IAAI,OAAO,CAAC,mBAAmB,CAAC;IAIrD,8DAA8D;IACxD,oBAAoB,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;IAQxD,2EAA2E;IACrE,qBAAqB,CACzB,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACpC,OAAO,CAAC,kBAAkB,CAAC;IAS9B,yDAAyD;IACnD,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAQzD,kGAAkG;IAC5F,mBAAmB,CACvB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,eAAe,CAAC;IAQ3B,yEAAyE;IACnE,YAAY,CAAC,CAAC,EAClB,MAAM,EAAE,iBAAiB,EACzB,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,GACrB,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;IAKzB,iDAAiD;IAC3C,SAAS,IAAI,OAAO,CAAC,cAAc,CAAC;YAI5B,OAAO;IAmGrB,OAAO,CAAC,YAAY;CAQrB"}
|
package/dist/fold.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Event } from "./types";
|
|
2
|
+
/** Interface for folding events into a state. */
|
|
3
|
+
export interface EventFolder<S> {
|
|
4
|
+
/** Apply an event to the folder's internal state. Returns true if the event was relevant. */
|
|
5
|
+
apply(event: Event): boolean;
|
|
6
|
+
/** Finalize and return the accumulated state. Returns undefined if no relevant events were applied. */
|
|
7
|
+
finalize(): S | undefined;
|
|
8
|
+
}
|
|
9
|
+
/** Fold a sequence of events using the given folder. */
|
|
10
|
+
export declare function foldEvents<S>(folder: EventFolder<S>, events: Event[]): S | undefined;
|
|
11
|
+
//# sourceMappingURL=fold.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fold.d.ts","sourceRoot":"","sources":["../src/fold.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAErC,iDAAiD;AACjD,MAAM,WAAW,WAAW,CAAC,CAAC;IAC5B,6FAA6F;IAC7F,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC;IAC7B,uGAAuG;IACvG,QAAQ,IAAI,CAAC,GAAG,SAAS,CAAC;CAC3B;AAED,wDAAwD;AACxD,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,CAAC,GAAG,SAAS,CAKpF"}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __moduleCache = /* @__PURE__ */ new WeakMap;
|
|
6
|
+
var __toCommonJS = (from) => {
|
|
7
|
+
var entry = __moduleCache.get(from), desc;
|
|
8
|
+
if (entry)
|
|
9
|
+
return entry;
|
|
10
|
+
entry = __defProp({}, "__esModule", { value: true });
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function")
|
|
12
|
+
__getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
|
|
13
|
+
get: () => from[key],
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
}));
|
|
16
|
+
__moduleCache.set(from, entry);
|
|
17
|
+
return entry;
|
|
18
|
+
};
|
|
19
|
+
var __export = (target, all) => {
|
|
20
|
+
for (var name in all)
|
|
21
|
+
__defProp(target, name, {
|
|
22
|
+
get: all[name],
|
|
23
|
+
enumerable: true,
|
|
24
|
+
configurable: true,
|
|
25
|
+
set: (newValue) => all[name] = () => newValue
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// src/index.ts
|
|
30
|
+
var exports_src = {};
|
|
31
|
+
__export(exports_src, {
|
|
32
|
+
foldEvents: () => foldEvents,
|
|
33
|
+
CircuitOpenError: () => CircuitOpenError,
|
|
34
|
+
CircuitBreaker: () => CircuitBreaker,
|
|
35
|
+
AllSourceError: () => AllSourceError,
|
|
36
|
+
AllSourceClient: () => AllSourceClient
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(exports_src);
|
|
39
|
+
|
|
40
|
+
// src/types.ts
|
|
41
|
+
class AllSourceError extends Error {
|
|
42
|
+
status;
|
|
43
|
+
body;
|
|
44
|
+
constructor(message, status, body) {
|
|
45
|
+
super(message);
|
|
46
|
+
this.status = status;
|
|
47
|
+
this.body = body;
|
|
48
|
+
this.name = "AllSourceError";
|
|
49
|
+
}
|
|
50
|
+
isUnauthorized() {
|
|
51
|
+
return this.status === 401;
|
|
52
|
+
}
|
|
53
|
+
isRateLimited() {
|
|
54
|
+
return this.status === 429;
|
|
55
|
+
}
|
|
56
|
+
isNotFound() {
|
|
57
|
+
return this.status === 404;
|
|
58
|
+
}
|
|
59
|
+
isForbidden() {
|
|
60
|
+
return this.status === 403;
|
|
61
|
+
}
|
|
62
|
+
isServerError() {
|
|
63
|
+
return this.status >= 500;
|
|
64
|
+
}
|
|
65
|
+
isRetryable() {
|
|
66
|
+
return [408, 429, 500, 502, 503, 504].includes(this.status);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
class CircuitOpenError extends Error {
|
|
71
|
+
constructor(message = "Circuit breaker is open") {
|
|
72
|
+
super(message);
|
|
73
|
+
this.name = "CircuitOpenError";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/circuit-breaker.ts
|
|
78
|
+
var DEFAULT_CONFIG = {
|
|
79
|
+
threshold: 5,
|
|
80
|
+
recoveryTimeout: 30000
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
class CircuitBreaker {
|
|
84
|
+
threshold;
|
|
85
|
+
recoveryTimeout;
|
|
86
|
+
consecutiveFailures = 0;
|
|
87
|
+
lastFailureTime = 0;
|
|
88
|
+
currentState = "closed";
|
|
89
|
+
constructor(config = {}) {
|
|
90
|
+
this.threshold = config.threshold ?? DEFAULT_CONFIG.threshold;
|
|
91
|
+
this.recoveryTimeout = config.recoveryTimeout ?? DEFAULT_CONFIG.recoveryTimeout;
|
|
92
|
+
}
|
|
93
|
+
get state() {
|
|
94
|
+
if (this.currentState === "open") {
|
|
95
|
+
const elapsed = Date.now() - this.lastFailureTime;
|
|
96
|
+
if (elapsed >= this.recoveryTimeout) {
|
|
97
|
+
return "half-open";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return this.currentState;
|
|
101
|
+
}
|
|
102
|
+
check() {
|
|
103
|
+
const s = this.state;
|
|
104
|
+
if (s === "open") {
|
|
105
|
+
throw new CircuitOpenError;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
recordSuccess() {
|
|
109
|
+
this.consecutiveFailures = 0;
|
|
110
|
+
this.currentState = "closed";
|
|
111
|
+
}
|
|
112
|
+
recordFailure() {
|
|
113
|
+
this.consecutiveFailures++;
|
|
114
|
+
this.lastFailureTime = Date.now();
|
|
115
|
+
if (this.consecutiveFailures >= this.threshold) {
|
|
116
|
+
this.currentState = "open";
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/fold.ts
|
|
122
|
+
function foldEvents(folder, events) {
|
|
123
|
+
for (const event of events) {
|
|
124
|
+
folder.apply(event);
|
|
125
|
+
}
|
|
126
|
+
return folder.finalize();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// src/client.ts
|
|
130
|
+
var DEFAULT_TIMEOUT = 30000;
|
|
131
|
+
var DEFAULT_RETRY = {
|
|
132
|
+
maxRetries: 3,
|
|
133
|
+
baseDelay: 200,
|
|
134
|
+
backoffFactor: 2,
|
|
135
|
+
maxDelay: 1e4
|
|
136
|
+
};
|
|
137
|
+
var RETRYABLE_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504]);
|
|
138
|
+
function normalizeCreated(raw) {
|
|
139
|
+
return {
|
|
140
|
+
id: raw.event_id ?? raw.id ?? "",
|
|
141
|
+
timestamp: raw.timestamp,
|
|
142
|
+
version: raw.version
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
class AllSourceClient {
|
|
147
|
+
baseUrl;
|
|
148
|
+
apiKey;
|
|
149
|
+
timeout;
|
|
150
|
+
retryConfig;
|
|
151
|
+
circuitBreaker;
|
|
152
|
+
fetch;
|
|
153
|
+
constructor(config) {
|
|
154
|
+
if (!config.baseUrl)
|
|
155
|
+
throw new Error("baseUrl is required");
|
|
156
|
+
if (!config.apiKey)
|
|
157
|
+
throw new Error("apiKey is required");
|
|
158
|
+
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
159
|
+
this.apiKey = config.apiKey;
|
|
160
|
+
this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
|
|
161
|
+
this.retryConfig = {
|
|
162
|
+
maxRetries: config.retry?.maxRetries ?? DEFAULT_RETRY.maxRetries,
|
|
163
|
+
baseDelay: config.retry?.baseDelay ?? DEFAULT_RETRY.baseDelay,
|
|
164
|
+
backoffFactor: config.retry?.backoffFactor ?? DEFAULT_RETRY.backoffFactor,
|
|
165
|
+
maxDelay: config.retry?.maxDelay ?? DEFAULT_RETRY.maxDelay
|
|
166
|
+
};
|
|
167
|
+
this.circuitBreaker = new CircuitBreaker(config.circuitBreaker);
|
|
168
|
+
this.fetch = config.fetch ?? globalThis.fetch;
|
|
169
|
+
}
|
|
170
|
+
async ingestEvent(event) {
|
|
171
|
+
const res = await this.request("POST", "/api/v1/events", event);
|
|
172
|
+
return normalizeCreated(res.data);
|
|
173
|
+
}
|
|
174
|
+
async ingestBatch(events) {
|
|
175
|
+
const res = await this.request("POST", "/api/v1/events/batch", { events });
|
|
176
|
+
return { count: res.count, events: (res.data ?? []).map(normalizeCreated) };
|
|
177
|
+
}
|
|
178
|
+
async queryEvents(params = {}) {
|
|
179
|
+
const query = new URLSearchParams;
|
|
180
|
+
for (const [key, value] of Object.entries(params)) {
|
|
181
|
+
if (value !== undefined && value !== null) {
|
|
182
|
+
query.set(key, String(value));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const qs = query.toString();
|
|
186
|
+
const path = qs ? `/api/v1/events/query?${qs}` : "/api/v1/events/query";
|
|
187
|
+
return this.request("GET", path);
|
|
188
|
+
}
|
|
189
|
+
async listProjections() {
|
|
190
|
+
return this.request("GET", "/api/v1/projections");
|
|
191
|
+
}
|
|
192
|
+
async listPrimeProjections() {
|
|
193
|
+
const res = await this.request("GET", "/api/v1/prime/projections");
|
|
194
|
+
return res.data;
|
|
195
|
+
}
|
|
196
|
+
async definePrimeProjection(entityType, fieldPolicies) {
|
|
197
|
+
const res = await this.request("POST", "/api/v1/prime/projections", { entity_type: entityType, field_policies: fieldPolicies });
|
|
198
|
+
return res.data;
|
|
199
|
+
}
|
|
200
|
+
async projectNode(nodeId) {
|
|
201
|
+
const res = await this.request("POST", `/api/v1/prime/nodes/${nodeId}/project`);
|
|
202
|
+
return res.data;
|
|
203
|
+
}
|
|
204
|
+
async nodeFieldProvenance(nodeId, field) {
|
|
205
|
+
const res = await this.request("GET", `/api/v1/prime/nodes/${nodeId}/fields/${field}/provenance`);
|
|
206
|
+
return res.data;
|
|
207
|
+
}
|
|
208
|
+
async queryAndFold(params, folder) {
|
|
209
|
+
const result = await this.queryEvents(params);
|
|
210
|
+
return foldEvents(folder, result.events);
|
|
211
|
+
}
|
|
212
|
+
async getHealth() {
|
|
213
|
+
return this.request("GET", "/health");
|
|
214
|
+
}
|
|
215
|
+
async request(method, path, body) {
|
|
216
|
+
this.circuitBreaker.check();
|
|
217
|
+
const url = `${this.baseUrl}${path}`;
|
|
218
|
+
const headers = {
|
|
219
|
+
"X-API-Key": this.apiKey,
|
|
220
|
+
Accept: "application/json"
|
|
221
|
+
};
|
|
222
|
+
if (body !== undefined) {
|
|
223
|
+
headers["Content-Type"] = "application/json";
|
|
224
|
+
}
|
|
225
|
+
let lastError;
|
|
226
|
+
const maxAttempts = this.retryConfig.maxRetries + 1;
|
|
227
|
+
for (let attempt = 0;attempt < maxAttempts; attempt++) {
|
|
228
|
+
if (attempt > 0) {
|
|
229
|
+
const delay = this.computeDelay(attempt);
|
|
230
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
231
|
+
}
|
|
232
|
+
const controller = new AbortController;
|
|
233
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
234
|
+
try {
|
|
235
|
+
const response = await this.fetch(url, {
|
|
236
|
+
method,
|
|
237
|
+
headers,
|
|
238
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
239
|
+
signal: controller.signal
|
|
240
|
+
});
|
|
241
|
+
if (!response.ok) {
|
|
242
|
+
const text = await response.text();
|
|
243
|
+
let responseBody;
|
|
244
|
+
try {
|
|
245
|
+
responseBody = JSON.parse(text);
|
|
246
|
+
} catch {
|
|
247
|
+
responseBody = text;
|
|
248
|
+
}
|
|
249
|
+
const error = new AllSourceError(`AllSource API error: ${response.status} ${response.statusText}`, response.status, responseBody);
|
|
250
|
+
if (RETRYABLE_STATUS_CODES.has(response.status) && attempt < maxAttempts - 1) {
|
|
251
|
+
lastError = error;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
this.circuitBreaker.recordFailure();
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
this.circuitBreaker.recordSuccess();
|
|
258
|
+
return await response.json();
|
|
259
|
+
} catch (error) {
|
|
260
|
+
if (error instanceof AllSourceError) {
|
|
261
|
+
if (error.isRetryable() && attempt < maxAttempts - 1) {
|
|
262
|
+
lastError = error;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
this.circuitBreaker.recordFailure();
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
269
|
+
const timeoutErr = new AllSourceError(`Request timeout after ${this.timeout}ms`, 0);
|
|
270
|
+
if (attempt < maxAttempts - 1) {
|
|
271
|
+
lastError = timeoutErr;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
this.circuitBreaker.recordFailure();
|
|
275
|
+
throw timeoutErr;
|
|
276
|
+
}
|
|
277
|
+
if (attempt < maxAttempts - 1) {
|
|
278
|
+
lastError = error;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
this.circuitBreaker.recordFailure();
|
|
282
|
+
throw error;
|
|
283
|
+
} finally {
|
|
284
|
+
clearTimeout(timer);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
this.circuitBreaker.recordFailure();
|
|
288
|
+
throw lastError;
|
|
289
|
+
}
|
|
290
|
+
computeDelay(attempt) {
|
|
291
|
+
const { baseDelay, backoffFactor, maxDelay } = this.retryConfig;
|
|
292
|
+
const exponentialDelay = baseDelay * Math.pow(backoffFactor, attempt - 1);
|
|
293
|
+
const capped = Math.min(exponentialDelay, maxDelay);
|
|
294
|
+
const jitter = Math.random() * capped;
|
|
295
|
+
return Math.floor(jitter);
|
|
296
|
+
}
|
|
297
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { AllSourceClient } from "./client";
|
|
2
|
+
export { CircuitBreaker } from "./circuit-breaker";
|
|
3
|
+
export type { CircuitState } from "./circuit-breaker";
|
|
4
|
+
export { foldEvents } from "./fold";
|
|
5
|
+
export type { EventFolder } from "./fold";
|
|
6
|
+
export { AllSourceError, CircuitOpenError, type AllSourceConfig, type CircuitBreakerConfig, type CreatedEvent, type Event, type HealthResponse, type IngestEventInput, type PrimeProjection, type PrimeProjectionAck, type PrimeProvenance, type PrimeSnapshot, type Projection, type ProjectionsResponse, type QueryEventsParams, type QueryEventsResponse, type RetryConfig, } from "./types";
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACpC,YAAY,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAC1C,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,YAAY,EACjB,KAAK,KAAK,EACV,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,WAAW,GACjB,MAAM,SAAS,CAAC"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
class AllSourceError extends Error {
|
|
3
|
+
status;
|
|
4
|
+
body;
|
|
5
|
+
constructor(message, status, body) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.status = status;
|
|
8
|
+
this.body = body;
|
|
9
|
+
this.name = "AllSourceError";
|
|
10
|
+
}
|
|
11
|
+
isUnauthorized() {
|
|
12
|
+
return this.status === 401;
|
|
13
|
+
}
|
|
14
|
+
isRateLimited() {
|
|
15
|
+
return this.status === 429;
|
|
16
|
+
}
|
|
17
|
+
isNotFound() {
|
|
18
|
+
return this.status === 404;
|
|
19
|
+
}
|
|
20
|
+
isForbidden() {
|
|
21
|
+
return this.status === 403;
|
|
22
|
+
}
|
|
23
|
+
isServerError() {
|
|
24
|
+
return this.status >= 500;
|
|
25
|
+
}
|
|
26
|
+
isRetryable() {
|
|
27
|
+
return [408, 429, 500, 502, 503, 504].includes(this.status);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class CircuitOpenError extends Error {
|
|
32
|
+
constructor(message = "Circuit breaker is open") {
|
|
33
|
+
super(message);
|
|
34
|
+
this.name = "CircuitOpenError";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/circuit-breaker.ts
|
|
39
|
+
var DEFAULT_CONFIG = {
|
|
40
|
+
threshold: 5,
|
|
41
|
+
recoveryTimeout: 30000
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
class CircuitBreaker {
|
|
45
|
+
threshold;
|
|
46
|
+
recoveryTimeout;
|
|
47
|
+
consecutiveFailures = 0;
|
|
48
|
+
lastFailureTime = 0;
|
|
49
|
+
currentState = "closed";
|
|
50
|
+
constructor(config = {}) {
|
|
51
|
+
this.threshold = config.threshold ?? DEFAULT_CONFIG.threshold;
|
|
52
|
+
this.recoveryTimeout = config.recoveryTimeout ?? DEFAULT_CONFIG.recoveryTimeout;
|
|
53
|
+
}
|
|
54
|
+
get state() {
|
|
55
|
+
if (this.currentState === "open") {
|
|
56
|
+
const elapsed = Date.now() - this.lastFailureTime;
|
|
57
|
+
if (elapsed >= this.recoveryTimeout) {
|
|
58
|
+
return "half-open";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return this.currentState;
|
|
62
|
+
}
|
|
63
|
+
check() {
|
|
64
|
+
const s = this.state;
|
|
65
|
+
if (s === "open") {
|
|
66
|
+
throw new CircuitOpenError;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
recordSuccess() {
|
|
70
|
+
this.consecutiveFailures = 0;
|
|
71
|
+
this.currentState = "closed";
|
|
72
|
+
}
|
|
73
|
+
recordFailure() {
|
|
74
|
+
this.consecutiveFailures++;
|
|
75
|
+
this.lastFailureTime = Date.now();
|
|
76
|
+
if (this.consecutiveFailures >= this.threshold) {
|
|
77
|
+
this.currentState = "open";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/fold.ts
|
|
83
|
+
function foldEvents(folder, events) {
|
|
84
|
+
for (const event of events) {
|
|
85
|
+
folder.apply(event);
|
|
86
|
+
}
|
|
87
|
+
return folder.finalize();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/client.ts
|
|
91
|
+
var DEFAULT_TIMEOUT = 30000;
|
|
92
|
+
var DEFAULT_RETRY = {
|
|
93
|
+
maxRetries: 3,
|
|
94
|
+
baseDelay: 200,
|
|
95
|
+
backoffFactor: 2,
|
|
96
|
+
maxDelay: 1e4
|
|
97
|
+
};
|
|
98
|
+
var RETRYABLE_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504]);
|
|
99
|
+
function normalizeCreated(raw) {
|
|
100
|
+
return {
|
|
101
|
+
id: raw.event_id ?? raw.id ?? "",
|
|
102
|
+
timestamp: raw.timestamp,
|
|
103
|
+
version: raw.version
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
class AllSourceClient {
|
|
108
|
+
baseUrl;
|
|
109
|
+
apiKey;
|
|
110
|
+
timeout;
|
|
111
|
+
retryConfig;
|
|
112
|
+
circuitBreaker;
|
|
113
|
+
fetch;
|
|
114
|
+
constructor(config) {
|
|
115
|
+
if (!config.baseUrl)
|
|
116
|
+
throw new Error("baseUrl is required");
|
|
117
|
+
if (!config.apiKey)
|
|
118
|
+
throw new Error("apiKey is required");
|
|
119
|
+
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
120
|
+
this.apiKey = config.apiKey;
|
|
121
|
+
this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
|
|
122
|
+
this.retryConfig = {
|
|
123
|
+
maxRetries: config.retry?.maxRetries ?? DEFAULT_RETRY.maxRetries,
|
|
124
|
+
baseDelay: config.retry?.baseDelay ?? DEFAULT_RETRY.baseDelay,
|
|
125
|
+
backoffFactor: config.retry?.backoffFactor ?? DEFAULT_RETRY.backoffFactor,
|
|
126
|
+
maxDelay: config.retry?.maxDelay ?? DEFAULT_RETRY.maxDelay
|
|
127
|
+
};
|
|
128
|
+
this.circuitBreaker = new CircuitBreaker(config.circuitBreaker);
|
|
129
|
+
this.fetch = config.fetch ?? globalThis.fetch;
|
|
130
|
+
}
|
|
131
|
+
async ingestEvent(event) {
|
|
132
|
+
const res = await this.request("POST", "/api/v1/events", event);
|
|
133
|
+
return normalizeCreated(res.data);
|
|
134
|
+
}
|
|
135
|
+
async ingestBatch(events) {
|
|
136
|
+
const res = await this.request("POST", "/api/v1/events/batch", { events });
|
|
137
|
+
return { count: res.count, events: (res.data ?? []).map(normalizeCreated) };
|
|
138
|
+
}
|
|
139
|
+
async queryEvents(params = {}) {
|
|
140
|
+
const query = new URLSearchParams;
|
|
141
|
+
for (const [key, value] of Object.entries(params)) {
|
|
142
|
+
if (value !== undefined && value !== null) {
|
|
143
|
+
query.set(key, String(value));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const qs = query.toString();
|
|
147
|
+
const path = qs ? `/api/v1/events/query?${qs}` : "/api/v1/events/query";
|
|
148
|
+
return this.request("GET", path);
|
|
149
|
+
}
|
|
150
|
+
async listProjections() {
|
|
151
|
+
return this.request("GET", "/api/v1/projections");
|
|
152
|
+
}
|
|
153
|
+
async listPrimeProjections() {
|
|
154
|
+
const res = await this.request("GET", "/api/v1/prime/projections");
|
|
155
|
+
return res.data;
|
|
156
|
+
}
|
|
157
|
+
async definePrimeProjection(entityType, fieldPolicies) {
|
|
158
|
+
const res = await this.request("POST", "/api/v1/prime/projections", { entity_type: entityType, field_policies: fieldPolicies });
|
|
159
|
+
return res.data;
|
|
160
|
+
}
|
|
161
|
+
async projectNode(nodeId) {
|
|
162
|
+
const res = await this.request("POST", `/api/v1/prime/nodes/${nodeId}/project`);
|
|
163
|
+
return res.data;
|
|
164
|
+
}
|
|
165
|
+
async nodeFieldProvenance(nodeId, field) {
|
|
166
|
+
const res = await this.request("GET", `/api/v1/prime/nodes/${nodeId}/fields/${field}/provenance`);
|
|
167
|
+
return res.data;
|
|
168
|
+
}
|
|
169
|
+
async queryAndFold(params, folder) {
|
|
170
|
+
const result = await this.queryEvents(params);
|
|
171
|
+
return foldEvents(folder, result.events);
|
|
172
|
+
}
|
|
173
|
+
async getHealth() {
|
|
174
|
+
return this.request("GET", "/health");
|
|
175
|
+
}
|
|
176
|
+
async request(method, path, body) {
|
|
177
|
+
this.circuitBreaker.check();
|
|
178
|
+
const url = `${this.baseUrl}${path}`;
|
|
179
|
+
const headers = {
|
|
180
|
+
"X-API-Key": this.apiKey,
|
|
181
|
+
Accept: "application/json"
|
|
182
|
+
};
|
|
183
|
+
if (body !== undefined) {
|
|
184
|
+
headers["Content-Type"] = "application/json";
|
|
185
|
+
}
|
|
186
|
+
let lastError;
|
|
187
|
+
const maxAttempts = this.retryConfig.maxRetries + 1;
|
|
188
|
+
for (let attempt = 0;attempt < maxAttempts; attempt++) {
|
|
189
|
+
if (attempt > 0) {
|
|
190
|
+
const delay = this.computeDelay(attempt);
|
|
191
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
192
|
+
}
|
|
193
|
+
const controller = new AbortController;
|
|
194
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
195
|
+
try {
|
|
196
|
+
const response = await this.fetch(url, {
|
|
197
|
+
method,
|
|
198
|
+
headers,
|
|
199
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
200
|
+
signal: controller.signal
|
|
201
|
+
});
|
|
202
|
+
if (!response.ok) {
|
|
203
|
+
const text = await response.text();
|
|
204
|
+
let responseBody;
|
|
205
|
+
try {
|
|
206
|
+
responseBody = JSON.parse(text);
|
|
207
|
+
} catch {
|
|
208
|
+
responseBody = text;
|
|
209
|
+
}
|
|
210
|
+
const error = new AllSourceError(`AllSource API error: ${response.status} ${response.statusText}`, response.status, responseBody);
|
|
211
|
+
if (RETRYABLE_STATUS_CODES.has(response.status) && attempt < maxAttempts - 1) {
|
|
212
|
+
lastError = error;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
this.circuitBreaker.recordFailure();
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
this.circuitBreaker.recordSuccess();
|
|
219
|
+
return await response.json();
|
|
220
|
+
} catch (error) {
|
|
221
|
+
if (error instanceof AllSourceError) {
|
|
222
|
+
if (error.isRetryable() && attempt < maxAttempts - 1) {
|
|
223
|
+
lastError = error;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
this.circuitBreaker.recordFailure();
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
230
|
+
const timeoutErr = new AllSourceError(`Request timeout after ${this.timeout}ms`, 0);
|
|
231
|
+
if (attempt < maxAttempts - 1) {
|
|
232
|
+
lastError = timeoutErr;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
this.circuitBreaker.recordFailure();
|
|
236
|
+
throw timeoutErr;
|
|
237
|
+
}
|
|
238
|
+
if (attempt < maxAttempts - 1) {
|
|
239
|
+
lastError = error;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
this.circuitBreaker.recordFailure();
|
|
243
|
+
throw error;
|
|
244
|
+
} finally {
|
|
245
|
+
clearTimeout(timer);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
this.circuitBreaker.recordFailure();
|
|
249
|
+
throw lastError;
|
|
250
|
+
}
|
|
251
|
+
computeDelay(attempt) {
|
|
252
|
+
const { baseDelay, backoffFactor, maxDelay } = this.retryConfig;
|
|
253
|
+
const exponentialDelay = baseDelay * Math.pow(backoffFactor, attempt - 1);
|
|
254
|
+
const capped = Math.min(exponentialDelay, maxDelay);
|
|
255
|
+
const jitter = Math.random() * capped;
|
|
256
|
+
return Math.floor(jitter);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
export {
|
|
260
|
+
foldEvents,
|
|
261
|
+
CircuitOpenError,
|
|
262
|
+
CircuitBreaker,
|
|
263
|
+
AllSourceError,
|
|
264
|
+
AllSourceClient
|
|
265
|
+
};
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/** Retry configuration with exponential backoff. */
|
|
2
|
+
export interface RetryConfig {
|
|
3
|
+
/** Maximum number of retries. Defaults to 3. */
|
|
4
|
+
maxRetries: number;
|
|
5
|
+
/** Base delay in milliseconds. Defaults to 200. */
|
|
6
|
+
baseDelay: number;
|
|
7
|
+
/** Backoff multiplier. Defaults to 2.0. */
|
|
8
|
+
backoffFactor: number;
|
|
9
|
+
/** Maximum delay in milliseconds. Defaults to 10000. */
|
|
10
|
+
maxDelay: number;
|
|
11
|
+
}
|
|
12
|
+
/** Circuit breaker configuration. */
|
|
13
|
+
export interface CircuitBreakerConfig {
|
|
14
|
+
/** Number of consecutive failures before opening the circuit. Defaults to 5. */
|
|
15
|
+
threshold: number;
|
|
16
|
+
/** Milliseconds to wait before entering half-open state. Defaults to 30000. */
|
|
17
|
+
recoveryTimeout: number;
|
|
18
|
+
}
|
|
19
|
+
/** Configuration options for the AllSource client. */
|
|
20
|
+
export interface AllSourceConfig {
|
|
21
|
+
/** Base URL of the AllSource Query Service (e.g., "https://allsource-query.fly.dev"). */
|
|
22
|
+
baseUrl: string;
|
|
23
|
+
/** API key for authentication (sent as X-API-Key header). */
|
|
24
|
+
apiKey: string;
|
|
25
|
+
/** Request timeout in milliseconds. Defaults to 30000. */
|
|
26
|
+
timeout?: number;
|
|
27
|
+
/** Retry configuration. Uses sensible defaults if omitted. */
|
|
28
|
+
retry?: Partial<RetryConfig>;
|
|
29
|
+
/** Circuit breaker configuration. Uses sensible defaults if omitted. */
|
|
30
|
+
circuitBreaker?: Partial<CircuitBreakerConfig>;
|
|
31
|
+
/** Custom fetch function. Defaults to globalThis.fetch. */
|
|
32
|
+
fetch?: typeof globalThis.fetch;
|
|
33
|
+
}
|
|
34
|
+
/** An event to ingest into AllSource. */
|
|
35
|
+
export interface IngestEventInput {
|
|
36
|
+
/** The type of event (e.g., "user.signup", "order.placed"). */
|
|
37
|
+
event_type: string;
|
|
38
|
+
/** The entity this event belongs to (e.g., user ID, order ID). */
|
|
39
|
+
entity_id: string;
|
|
40
|
+
/** Arbitrary JSON payload for the event. */
|
|
41
|
+
payload: Record<string, unknown>;
|
|
42
|
+
/** Optional metadata (e.g., source, version, ip). */
|
|
43
|
+
metadata?: Record<string, unknown>;
|
|
44
|
+
}
|
|
45
|
+
/** A stored event returned from AllSource. */
|
|
46
|
+
export interface Event {
|
|
47
|
+
id: string;
|
|
48
|
+
event_type: string;
|
|
49
|
+
entity_id: string;
|
|
50
|
+
payload: Record<string, unknown>;
|
|
51
|
+
metadata: Record<string, unknown>;
|
|
52
|
+
timestamp: string;
|
|
53
|
+
stream_id?: string;
|
|
54
|
+
version?: number;
|
|
55
|
+
tenant_id?: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Acknowledgement for a created event, returned by `ingestEvent` and (per item)
|
|
59
|
+
* `ingestBatch`. The gateway keys the id as `event_id`; the SDK normalizes it
|
|
60
|
+
* to `id` for parity with queried events.
|
|
61
|
+
*/
|
|
62
|
+
export interface CreatedEvent {
|
|
63
|
+
/** The stored event's id. */
|
|
64
|
+
id: string;
|
|
65
|
+
/** Server-assigned timestamp (ISO 8601). */
|
|
66
|
+
timestamp: string;
|
|
67
|
+
/** Monotonic version assigned by Core, when provided. */
|
|
68
|
+
version?: number;
|
|
69
|
+
}
|
|
70
|
+
/** Query parameters for filtering events. */
|
|
71
|
+
export interface QueryEventsParams {
|
|
72
|
+
/** Filter by entity ID. */
|
|
73
|
+
entity_id?: string;
|
|
74
|
+
/** Filter by event type. */
|
|
75
|
+
event_type?: string;
|
|
76
|
+
/** Maximum number of events to return. */
|
|
77
|
+
limit?: number;
|
|
78
|
+
/** Number of events to skip. */
|
|
79
|
+
offset?: number;
|
|
80
|
+
/** Start time filter (ISO 8601). */
|
|
81
|
+
since?: string;
|
|
82
|
+
/** End time filter (ISO 8601). */
|
|
83
|
+
until?: string;
|
|
84
|
+
}
|
|
85
|
+
/** Response from querying events. */
|
|
86
|
+
export interface QueryEventsResponse {
|
|
87
|
+
events: Event[];
|
|
88
|
+
count: number;
|
|
89
|
+
}
|
|
90
|
+
/** A projection returned from AllSource Core. */
|
|
91
|
+
export interface Projection {
|
|
92
|
+
name: string;
|
|
93
|
+
state?: unknown;
|
|
94
|
+
[key: string]: unknown;
|
|
95
|
+
}
|
|
96
|
+
/** Response from listing projections. */
|
|
97
|
+
export interface ProjectionsResponse {
|
|
98
|
+
projections: Projection[];
|
|
99
|
+
total: number;
|
|
100
|
+
}
|
|
101
|
+
/** A Prime projection definition (entity type plus per-field merge policies). */
|
|
102
|
+
export interface PrimeProjection {
|
|
103
|
+
entity_type: string;
|
|
104
|
+
field_policies: Record<string, string>;
|
|
105
|
+
}
|
|
106
|
+
/** Acknowledgement returned when defining a Prime projection. */
|
|
107
|
+
export interface PrimeProjectionAck {
|
|
108
|
+
entity_type: string;
|
|
109
|
+
persisted: boolean;
|
|
110
|
+
}
|
|
111
|
+
/** A materialized Prime node snapshot. */
|
|
112
|
+
export interface PrimeSnapshot {
|
|
113
|
+
entity_type: string;
|
|
114
|
+
fields: Record<string, unknown>;
|
|
115
|
+
observation_count: number;
|
|
116
|
+
}
|
|
117
|
+
/** Provenance for a single field on a Prime node. */
|
|
118
|
+
export interface PrimeProvenance {
|
|
119
|
+
field: string;
|
|
120
|
+
value: unknown;
|
|
121
|
+
source_event_id: string;
|
|
122
|
+
source_event_at: string;
|
|
123
|
+
merge_policy_applied: string;
|
|
124
|
+
}
|
|
125
|
+
/** Response from the health endpoint. */
|
|
126
|
+
export interface HealthResponse {
|
|
127
|
+
status: string;
|
|
128
|
+
[key: string]: unknown;
|
|
129
|
+
}
|
|
130
|
+
/** Error thrown by the AllSource client. */
|
|
131
|
+
export declare class AllSourceError extends Error {
|
|
132
|
+
readonly status: number;
|
|
133
|
+
readonly body?: unknown | undefined;
|
|
134
|
+
constructor(message: string, status: number, body?: unknown | undefined);
|
|
135
|
+
/** Whether the error is a 401 Unauthorized. */
|
|
136
|
+
isUnauthorized(): boolean;
|
|
137
|
+
/** Whether the error is a 429 Rate Limited. */
|
|
138
|
+
isRateLimited(): boolean;
|
|
139
|
+
/** Whether the error is a 404 Not Found. */
|
|
140
|
+
isNotFound(): boolean;
|
|
141
|
+
/** Whether the error is a 403 Forbidden. */
|
|
142
|
+
isForbidden(): boolean;
|
|
143
|
+
/** Whether the error is a server error (5xx). */
|
|
144
|
+
isServerError(): boolean;
|
|
145
|
+
/** Whether the error is retryable (408, 429, 500, 502, 503, 504). */
|
|
146
|
+
isRetryable(): boolean;
|
|
147
|
+
}
|
|
148
|
+
/** Error thrown when the circuit breaker is open. */
|
|
149
|
+
export declare class CircuitOpenError extends Error {
|
|
150
|
+
constructor(message?: string);
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@allsourcedev/client",
|
|
3
|
+
"version": "0.23.0",
|
|
4
|
+
"description": "JavaScript/TypeScript client for the AllSource event store API",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.mjs",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.mjs",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "bun build src/index.ts --outfile dist/index.mjs --target node --format esm && bun build src/index.ts --outfile dist/index.cjs --target node --format cjs && bun run build:types",
|
|
22
|
+
"build:types": "tsc --emitDeclarationOnly --declaration --outDir dist",
|
|
23
|
+
"type-check": "tsc --noEmit",
|
|
24
|
+
"test": "bun test",
|
|
25
|
+
"smoke": "bun run scripts/smoke.ts",
|
|
26
|
+
"clean": "rm -rf dist .turbo",
|
|
27
|
+
"prepublishOnly": "bun run build"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/bun": "^1.1.13",
|
|
31
|
+
"typescript": "^6.0.2"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"allsource",
|
|
35
|
+
"event-store",
|
|
36
|
+
"event-sourcing",
|
|
37
|
+
"typescript",
|
|
38
|
+
"api-client"
|
|
39
|
+
],
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "https://github.com/all-source-os/all-source",
|
|
43
|
+
"directory": "sdks/typescript"
|
|
44
|
+
}
|
|
45
|
+
}
|