@syncular/server 0.32.1 → 0.33.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/dist/blob-handlers.js +3 -3
- package/dist/bucket-blob-store.d.ts +77 -0
- package/dist/bucket-blob-store.js +266 -0
- package/dist/bucket-segment-store.d.ts +77 -0
- package/dist/bucket-segment-store.js +445 -0
- package/dist/context.d.ts +7 -0
- package/dist/handler.js +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/object-bucket.d.ts +78 -0
- package/dist/object-bucket.js +18 -0
- package/dist/operations-realtime.js +3 -0
- package/dist/realtime.d.ts +11 -0
- package/dist/realtime.js +25 -58
- package/dist/s3-blob-store.d.ts +9 -64
- package/dist/s3-blob-store.js +25 -359
- package/dist/s3-object-bucket.d.ts +39 -0
- package/dist/s3-object-bucket.js +164 -0
- package/dist/s3-segment-store.d.ts +9 -61
- package/dist/s3-segment-store.js +25 -503
- package/package.json +2 -2
- package/src/blob-handlers.ts +4 -4
- package/src/bucket-blob-store.ts +351 -0
- package/src/bucket-segment-store.ts +582 -0
- package/src/context.ts +10 -0
- package/src/handler.ts +11 -0
- package/src/index.ts +4 -0
- package/src/object-bucket.ts +83 -0
- package/src/operations-realtime.ts +7 -0
- package/src/realtime.ts +29 -58
- package/src/s3-blob-store.ts +25 -466
- package/src/s3-object-bucket.ts +233 -0
- package/src/s3-segment-store.ts +25 -646
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The object-bucket seam: the handful of object-store operations the durable
|
|
3
|
+
* segment store (§5.1) and blob store (§5.9.2) actually perform, expressed
|
|
4
|
+
* without an HTTP transport.
|
|
5
|
+
*
|
|
6
|
+
* Two adapters implement it — `S3ObjectBucket` (`s3-object-bucket.ts`,
|
|
7
|
+
* hand-rolled SigV4 over `fetch`) and the R2 binding adapter in
|
|
8
|
+
* `@syncular/server-workers` (`r2-stores.ts`, the Worker's `env.BUCKET`) — so
|
|
9
|
+
* one implementation of the store logic (`bucket-segment-store.ts`,
|
|
10
|
+
* `bucket-blob-store.ts`) runs over both. The key layouts are the same on
|
|
11
|
+
* either transport, so a bucket can be served through the S3 API for one host
|
|
12
|
+
* and the binding for another.
|
|
13
|
+
*
|
|
14
|
+
* Everything here is structural: an adapter may be any object with these
|
|
15
|
+
* methods, and Cloudflare's `R2Bucket` is assignable to the declaration in
|
|
16
|
+
* `r2-stores.ts` without a dependency on `@cloudflare/workers-types`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** One stored object, readable once. Mirrors `R2ObjectBody`. */
|
|
20
|
+
export interface BucketObject {
|
|
21
|
+
/** The body, streamed; `null` only if the backend served none. */
|
|
22
|
+
readonly body: ReadableStream<Uint8Array<ArrayBuffer>> | null;
|
|
23
|
+
/** Backend ETag of the stored object (opaque, used as a CAS token). */
|
|
24
|
+
readonly etag: string | undefined;
|
|
25
|
+
/** Stored size in bytes, when the backend reports one. */
|
|
26
|
+
readonly size: number | undefined;
|
|
27
|
+
/** User metadata: the `x-amz-meta-*` headers S3-side, `customMetadata` on R2. */
|
|
28
|
+
readonly metadata: Readonly<Record<string, string>>;
|
|
29
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
30
|
+
text(): Promise<string>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** One stored object's description without its body. Mirrors `R2Object`. */
|
|
34
|
+
export interface BucketObjectHead {
|
|
35
|
+
readonly etag: string | undefined;
|
|
36
|
+
readonly size: number | undefined;
|
|
37
|
+
readonly metadata: Readonly<Record<string, string>>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface BucketWriteOptions {
|
|
41
|
+
readonly body: Uint8Array;
|
|
42
|
+
readonly contentType: string;
|
|
43
|
+
/** User metadata; values must be ASCII (both backends require it). */
|
|
44
|
+
readonly metadata?: Readonly<Record<string, string>> | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* Create-only write (`If-None-Match: *`, R2 `etagDoesNotMatch: '*'`). The
|
|
47
|
+
* content-addressed bytes object is written this way so a second publisher
|
|
48
|
+
* of identical bytes is a no-op and a hash collision cannot overwrite.
|
|
49
|
+
*/
|
|
50
|
+
readonly createOnly?: boolean | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* Compare-and-swap write (`If-Match`, R2 `etagMatches`): the write lands
|
|
53
|
+
* only if the stored ETag still equals this. The read-modify-write of the
|
|
54
|
+
* segment record and of the stats accumulator depend on it.
|
|
55
|
+
*/
|
|
56
|
+
readonly ifMatch?: string | undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** One page of keys under a prefix. `cursor` is set when more remain. */
|
|
60
|
+
export interface BucketListPage {
|
|
61
|
+
readonly keys: readonly string[];
|
|
62
|
+
readonly cursor: string | undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ObjectBucket {
|
|
66
|
+
/** The object plus its bytes, or `undefined` when the key is absent. */
|
|
67
|
+
get(key: string): Promise<BucketObject | undefined>;
|
|
68
|
+
head(key: string): Promise<BucketObjectHead | undefined>;
|
|
69
|
+
/**
|
|
70
|
+
* Write one object. Returns `false` when the `createOnly` / `ifMatch`
|
|
71
|
+
* precondition failed, so the caller can re-read and retry; a backend that
|
|
72
|
+
* ignores conditional writes would instead silently lose a concurrent
|
|
73
|
+
* update, which is why the return value is not optional.
|
|
74
|
+
*/
|
|
75
|
+
put(key: string, options: BucketWriteOptions): Promise<boolean>;
|
|
76
|
+
/** Delete one object. Deleting an absent key succeeds (idempotent). */
|
|
77
|
+
delete(key: string): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* List keys under a prefix, one page at a time. The blob orphan sweep
|
|
80
|
+
* (§5.9.2) is the only caller: everything on the hot path is a GET/HEAD.
|
|
81
|
+
*/
|
|
82
|
+
list(prefix: string, cursor?: string): Promise<BucketListPage>;
|
|
83
|
+
}
|
|
@@ -271,5 +271,12 @@ export function composeRealtimeNotifiers(
|
|
|
271
271
|
notifiers.map((notifier) => notifier.notifyCommit(partition, commit)),
|
|
272
272
|
);
|
|
273
273
|
},
|
|
274
|
+
notifySubscriptions: async (partition, clientId) => {
|
|
275
|
+
await Promise.all(
|
|
276
|
+
notifiers.map((notifier) =>
|
|
277
|
+
notifier.notifySubscriptions?.(partition, clientId),
|
|
278
|
+
),
|
|
279
|
+
);
|
|
280
|
+
},
|
|
274
281
|
};
|
|
275
282
|
}
|
package/src/realtime.ts
CHANGED
|
@@ -692,6 +692,15 @@ export class RealtimeSession {
|
|
|
692
692
|
* deltas immediately; it also survives a newer round-end refresh that
|
|
693
693
|
* failed, because that round applied nothing.
|
|
694
694
|
*/
|
|
695
|
+
/**
|
|
696
|
+
* A pull outside this connection changed the client's subscriptions. A
|
|
697
|
+
* round in flight on the connection reloads them at its end anyway.
|
|
698
|
+
*/
|
|
699
|
+
async refreshAfterPull(): Promise<void> {
|
|
700
|
+
if (this.#activeRound !== undefined) return;
|
|
701
|
+
await this.refreshRegistrations(false);
|
|
702
|
+
}
|
|
703
|
+
|
|
695
704
|
async refreshRegistrations(failClosed: boolean): Promise<void> {
|
|
696
705
|
const generation = ++this.#registrationGeneration;
|
|
697
706
|
let registrations: Registration[];
|
|
@@ -1104,67 +1113,13 @@ export class RealtimeHub {
|
|
|
1104
1113
|
'socket sync rounds require a segment store on the realtime hub (§8.7)',
|
|
1105
1114
|
);
|
|
1106
1115
|
}
|
|
1116
|
+
// The whole config, so a new `SyncServerConfig` field reaches socket and
|
|
1117
|
+
// hub-served HTTP rounds without being listed here.
|
|
1107
1118
|
return {
|
|
1119
|
+
...this.#config,
|
|
1120
|
+
segments,
|
|
1108
1121
|
partition: identity.partition,
|
|
1109
1122
|
actorId: identity.actorId,
|
|
1110
|
-
schema: this.#config.schema,
|
|
1111
|
-
...(this.#config.schemaWindow !== undefined
|
|
1112
|
-
? { schemaWindow: this.#config.schemaWindow }
|
|
1113
|
-
: {}),
|
|
1114
|
-
storage: this.#config.storage,
|
|
1115
|
-
segments,
|
|
1116
|
-
resolveScopes: this.#config.resolveScopes,
|
|
1117
|
-
...(this.#config.blobs !== undefined
|
|
1118
|
-
? { blobs: this.#config.blobs }
|
|
1119
|
-
: {}),
|
|
1120
|
-
...(this.#config.maxBlobBytes !== undefined
|
|
1121
|
-
? { maxBlobBytes: this.#config.maxBlobBytes }
|
|
1122
|
-
: {}),
|
|
1123
|
-
...(this.#config.crdtMergers !== undefined
|
|
1124
|
-
? { crdtMergers: this.#config.crdtMergers }
|
|
1125
|
-
: {}),
|
|
1126
|
-
...(this.#config.validators !== undefined
|
|
1127
|
-
? { validators: this.#config.validators }
|
|
1128
|
-
: {}),
|
|
1129
|
-
...(this.#config.commitValidator !== undefined
|
|
1130
|
-
? { commitValidator: this.#config.commitValidator }
|
|
1131
|
-
: {}),
|
|
1132
|
-
...(this.#config.reactionPlanner !== undefined
|
|
1133
|
-
? { reactionPlanner: this.#config.reactionPlanner }
|
|
1134
|
-
: {}),
|
|
1135
|
-
...(this.#config.checkpoints !== undefined
|
|
1136
|
-
? { checkpoints: this.#config.checkpoints }
|
|
1137
|
-
: {}),
|
|
1138
|
-
...(this.#config.clock !== undefined
|
|
1139
|
-
? { clock: this.#config.clock }
|
|
1140
|
-
: {}),
|
|
1141
|
-
...(this.#config.limits !== undefined
|
|
1142
|
-
? { limits: this.#config.limits }
|
|
1143
|
-
: {}),
|
|
1144
|
-
...(this.#config.signedUrls !== undefined
|
|
1145
|
-
? { signedUrls: this.#config.signedUrls }
|
|
1146
|
-
: {}),
|
|
1147
|
-
...(this.#config.blobSignedUrls !== undefined
|
|
1148
|
-
? { blobSignedUrls: this.#config.blobSignedUrls }
|
|
1149
|
-
: {}),
|
|
1150
|
-
...(this.#config.blobUploadUrls !== undefined
|
|
1151
|
-
? { blobUploadUrls: this.#config.blobUploadUrls }
|
|
1152
|
-
: {}),
|
|
1153
|
-
...(this.#config.sqliteImageBuilder !== undefined
|
|
1154
|
-
? { sqliteImageBuilder: this.#config.sqliteImageBuilder }
|
|
1155
|
-
: {}),
|
|
1156
|
-
...(this.#config.leases !== undefined
|
|
1157
|
-
? { leases: this.#config.leases }
|
|
1158
|
-
: {}),
|
|
1159
|
-
...(this.#config.events !== undefined
|
|
1160
|
-
? { events: this.#config.events }
|
|
1161
|
-
: {}),
|
|
1162
|
-
...(this.#config.onError !== undefined
|
|
1163
|
-
? { onError: this.#config.onError }
|
|
1164
|
-
: {}),
|
|
1165
|
-
...(this.#config.mapError !== undefined
|
|
1166
|
-
? { mapError: this.#config.mapError }
|
|
1167
|
-
: {}),
|
|
1168
1123
|
realtime: this,
|
|
1169
1124
|
};
|
|
1170
1125
|
}
|
|
@@ -1311,6 +1266,22 @@ export class RealtimeHub {
|
|
|
1311
1266
|
}
|
|
1312
1267
|
}
|
|
1313
1268
|
|
|
1269
|
+
/**
|
|
1270
|
+
* RealtimeNotifier: a pull changed a client's subscription list (§8.7).
|
|
1271
|
+
* Its sockets reload their registrations with the round-end semantics, so
|
|
1272
|
+
* an unreadable record keeps the previous registrations.
|
|
1273
|
+
*/
|
|
1274
|
+
async notifySubscriptions(
|
|
1275
|
+
partition: string,
|
|
1276
|
+
clientId: string,
|
|
1277
|
+
): Promise<void> {
|
|
1278
|
+
for (const session of [...this.#sessions]) {
|
|
1279
|
+
if (session.partition !== partition || session.clientId !== clientId)
|
|
1280
|
+
continue;
|
|
1281
|
+
await session.refreshAfterPull();
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1314
1285
|
/**
|
|
1315
1286
|
* Host-initiated, fail-closed registration refresh (§8.7). A host calls this
|
|
1316
1287
|
* after it changes an actor's membership or connection state, so the
|