Skip to content

Commit 49966a3

Browse files
committed
Add a minimum version check.
1 parent 0916456 commit 49966a3

File tree

4 files changed

+305
-167
lines changed

4 files changed

+305
-167
lines changed

packages/sdk/src/db_connection_builder.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { DbConnectionImpl, type ConnectionEvent } from './db_connection_impl';
22
import { EventEmitter } from './event_emitter';
33
import type { Identity } from './identity';
44
import type RemoteModule from './spacetime_module';
5+
import { ensureMinimumVersionOrThrow } from './version';
56
import { WebsocketDecompressAdapter } from './websocket_decompress_adapter';
67

78
/**
@@ -214,6 +215,13 @@ export class DbConnectionBuilder<
214215
'Database name or address is required to connect to SpacetimeDB'
215216
);
216217
}
218+
let versionString: string | undefined = undefined;
219+
if (this.remoteModule.versionInfo) {
220+
versionString = this.remoteModule.versionInfo.cliVersion;
221+
}
222+
// We could consider making this an `onConnectError` instead of throwing here.
223+
// Ideally, it would be a compile time error, but I'm not sure how to accomplish that.
224+
ensureMinimumVersionOrThrow(versionString);
217225

218226
return this.dbConnectionConstructor(
219227
new DbConnectionImpl({

packages/sdk/src/spacetime_module.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,7 @@ export default interface RemoteModule {
2222
setReducerFlags: any
2323
) => any;
2424
setReducerFlagsConstructor: () => any;
25+
versionInfo?: {
26+
cliVersion: string;
27+
};
2528
}

packages/sdk/src/version.ts

Lines changed: 116 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -2,103 +2,134 @@ export type PrereleaseId = string | number;
22

33
export type PreRelease = PrereleaseId[];
44

5-
6-
function comparePreReleases(
7-
a: PreRelease,
8-
b: PreRelease
9-
): number {
10-
const len = Math.min(a.length, b.length);
11-
for (let i = 0; i < len; i++) {
12-
const aPart = a[i];
13-
const bPart = b[i];
14-
if (aPart === bPart) continue;
15-
if (typeof aPart === 'number' && typeof bPart === 'number') {
16-
return aPart - bPart;
17-
}
18-
if (typeof aPart === 'string' && typeof bPart === 'string') {
19-
return aPart.localeCompare(bPart);
20-
}
21-
// According to 11.4.3, numeric identifiers always have lower precedence than non-numeric identifiers.
22-
// So if `a` is a string, it has higher precedence than `b`.
23-
return typeof aPart === 'string' ? 1 : -1;
5+
// Compare pre-release identifiers according to the semver spec (https://semver.org/#spec-item-11).
6+
function comparePreReleases(a: PreRelease, b: PreRelease): number {
7+
const len = Math.min(a.length, b.length);
8+
for (let i = 0; i < len; i++) {
9+
const aPart = a[i];
10+
const bPart = b[i];
11+
if (aPart === bPart) continue;
12+
if (typeof aPart === 'number' && typeof bPart === 'number') {
13+
return aPart - bPart;
14+
}
15+
if (typeof aPart === 'string' && typeof bPart === 'string') {
16+
return aPart.localeCompare(bPart);
2417
}
25-
// See rule 11.4.4 in the semver spec.
26-
return a.length - b.length;
18+
// According to item 11.4.3, numeric identifiers always have lower precedence than non-numeric identifiers.
19+
// So if `a` is a string, it has higher precedence than `b`.
20+
return typeof aPart === 'string' ? 1 : -1;
21+
}
22+
// See rule 11.4.4 in the semver spec.
23+
return a.length - b.length;
2724
}
2825

29-
// We don't use these, so I'm not going to parse it to spec.
26+
// We don't use these, and they don't matter for version ordering, so I'm not going to parse it to spec.
3027
export type BuildInfo = string;
3128

29+
// This is exported for tests.
3230
export class SemanticVersion {
33-
major: number;
34-
minor: number;
35-
patch: number;
36-
preRelease: PreRelease | null;
37-
buildInfo: BuildInfo | null;
38-
39-
constructor(
40-
major: number,
41-
minor: number,
42-
patch: number,
43-
preRelease: PreRelease | null = null,
44-
buildInfo: BuildInfo | null = null
45-
) {
46-
this.major = major;
47-
this.minor = minor;
48-
this.patch = patch;
49-
this.preRelease = preRelease;
50-
this.buildInfo = buildInfo;
31+
major: number;
32+
minor: number;
33+
patch: number;
34+
preRelease: PreRelease | null;
35+
buildInfo: BuildInfo | null;
36+
37+
constructor(
38+
major: number,
39+
minor: number,
40+
patch: number,
41+
preRelease: PreRelease | null = null,
42+
buildInfo: BuildInfo | null = null
43+
) {
44+
this.major = major;
45+
this.minor = minor;
46+
this.patch = patch;
47+
this.preRelease = preRelease;
48+
this.buildInfo = buildInfo;
49+
}
50+
51+
toString(): string {
52+
let versionString = `${this.major}.${this.minor}.${this.patch}`;
53+
if (this.preRelease) {
54+
versionString += `-${this.preRelease.join('.')}`;
5155
}
52-
53-
toString(): string {
54-
let versionString = `${this.major}.${this.minor}.${this.patch}`;
55-
if (this.preRelease) {
56-
versionString += `-${this.preRelease.join('.')}`;
57-
}
58-
if (this.buildInfo) {
59-
versionString += `+${this.buildInfo}`;
60-
}
61-
return versionString;
56+
if (this.buildInfo) {
57+
versionString += `+${this.buildInfo}`;
6258
}
59+
return versionString;
60+
}
6361

64-
compare(other: SemanticVersion): number {
65-
if (this.major !== other.major) {
66-
return this.major - other.major;
67-
}
68-
if (this.minor !== other.minor) {
69-
return this.minor - other.minor;
70-
}
71-
if (this.patch !== other.patch) {
72-
return this.patch - other.patch;
73-
}
74-
if (this.preRelease && other.preRelease) {
75-
return comparePreReleases(this.preRelease, other.preRelease);
76-
}
77-
if (this.preRelease) {
78-
return -1; // The version without a pre-release is greater.
79-
}
80-
if (other.preRelease) {
81-
return -1; // Since we don't have a pre-release, this version is greater.
82-
}
83-
return 0; // versions are equal
62+
compare(other: SemanticVersion): number {
63+
if (this.major !== other.major) {
64+
return this.major - other.major;
65+
}
66+
if (this.minor !== other.minor) {
67+
return this.minor - other.minor;
68+
}
69+
if (this.patch !== other.patch) {
70+
return this.patch - other.patch;
71+
}
72+
if (this.preRelease && other.preRelease) {
73+
return comparePreReleases(this.preRelease, other.preRelease);
74+
}
75+
if (this.preRelease) {
76+
return -1; // The version without a pre-release is greater.
8477
}
78+
if (other.preRelease) {
79+
return -1; // Since we don't have a pre-release, this version is greater.
80+
}
81+
return 0; // versions are equal
82+
}
83+
84+
clone(): SemanticVersion {
85+
return new SemanticVersion(
86+
this.major,
87+
this.minor,
88+
this.patch,
89+
this.preRelease ? [...this.preRelease] : null,
90+
this.buildInfo
91+
);
92+
}
8593

86-
static parseVersionString(version: string): SemanticVersion {
87-
const regex = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([\da-zA-Z-]+(?:\.[\da-zA-Z-]+)*))?(?:\+([\da-zA-Z-]+(?:\.[\da-zA-Z-]+)*))?$/;
88-
const match = version.match(regex);
89-
if (!match) {
90-
throw new Error(`Invalid version string: ${version}`);
91-
}
92-
93-
const major = parseInt(match[1], 10);
94-
const minor = parseInt(match[2], 10);
95-
const patch = parseInt(match[3], 10);
96-
const preRelease = match[4] ? match[4].split('.').map(id => isNaN(Number(id)) ? id : Number(id)) : null;
97-
const buildInfo = match[5] || null;
98-
99-
return new SemanticVersion(major, minor, patch, preRelease, buildInfo);
94+
static parseVersionString(version: string): SemanticVersion {
95+
const regex =
96+
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([\da-zA-Z-]+(?:\.[\da-zA-Z-]+)*))?(?:\+([\da-zA-Z-]+(?:\.[\da-zA-Z-]+)*))?$/;
97+
const match = version.match(regex);
98+
if (!match) {
99+
throw new Error(`Invalid version string: ${version}`);
100100
}
101+
102+
const major = parseInt(match[1], 10);
103+
const minor = parseInt(match[2], 10);
104+
const patch = parseInt(match[3], 10);
105+
const preRelease = match[4]
106+
? match[4].split('.').map(id => (isNaN(Number(id)) ? id : Number(id)))
107+
: null;
108+
const buildInfo = match[5] || null;
109+
110+
return new SemanticVersion(major, minor, patch, preRelease, buildInfo);
111+
}
101112
}
102113

103114
// The SDK depends on some module information that was not generated before this version.
104-
export const MINIMUM_CLI_VERSION: SemanticVersion = new SemanticVersion(1, 1, 2);
115+
export const _MINIMUM_CLI_VERSION: SemanticVersion = new SemanticVersion(
116+
1,
117+
1,
118+
2
119+
);
120+
121+
export function ensureMinimumVersionOrThrow(versionString?: string): void {
122+
if (versionString === undefined) {
123+
throw new Error(versionErrorMessage(versionString));
124+
}
125+
const version = SemanticVersion.parseVersionString(versionString);
126+
if (version.compare(_MINIMUM_CLI_VERSION) < 0) {
127+
throw new Error(versionErrorMessage(versionString));
128+
}
129+
}
130+
131+
function versionErrorMessage(incompatibleVersion?: string): string {
132+
const badVersion =
133+
incompatibleVersion === undefined ? 'unknown' : incompatibleVersion;
134+
return `Module code was generated with an incompatible version of the spacetimedb cli (${incompatibleVersion}). Update the cli version to at least ${_MINIMUM_CLI_VERSION.toString()} and regenerate the bindings. You can upgrade to the latest cli version by running: spacetime version upgrade`;
135+
}

0 commit comments

Comments
 (0)