@arcadible/sdk 0.0.0-stage → 0.3.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/LICENSE +176 -0
- package/README.md +61 -2
- package/dist/bus/client.d.ts +16 -0
- package/dist/bus/client.js +133 -0
- package/dist/bus/protocol.d.ts +68 -0
- package/dist/bus/protocol.js +140 -0
- package/dist/bus/server.d.ts +32 -0
- package/dist/bus/server.js +175 -0
- package/dist/client.d.ts +20 -0
- package/dist/client.js +42 -0
- package/dist/host.d.ts +23 -0
- package/dist/host.js +35 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +10 -0
- package/dist/saves/backend/indexeddb.d.ts +10 -0
- package/dist/saves/backend/indexeddb.js +224 -0
- package/dist/saves/backend/listeners.d.ts +6 -0
- package/dist/saves/backend/listeners.js +33 -0
- package/dist/saves/backend/memory.d.ts +7 -0
- package/dist/saves/backend/memory.js +51 -0
- package/dist/saves/backend/types.d.ts +27 -0
- package/dist/saves/backend/types.js +2 -0
- package/dist/saves/client.d.ts +16 -0
- package/dist/saves/client.js +210 -0
- package/dist/saves/rules.d.ts +35 -0
- package/dist/saves/rules.js +49 -0
- package/dist/saves/server.d.ts +11 -0
- package/dist/saves/server.js +120 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/package.json +45 -4
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Bus } from "../bus/client.js";
|
|
2
|
+
import { type SaveInfo } from "./rules.js";
|
|
3
|
+
export interface Saves {
|
|
4
|
+
write(name: string, data: string): Promise<SaveInfo>;
|
|
5
|
+
read(name: string): Promise<string | null>;
|
|
6
|
+
list(): Promise<string[]>;
|
|
7
|
+
info(name: string): Promise<SaveInfo | null>;
|
|
8
|
+
remove(name: string): Promise<void>;
|
|
9
|
+
readSync(name: string): string | null;
|
|
10
|
+
}
|
|
11
|
+
export declare function createSaves(bus: Bus, { timeout }: {
|
|
12
|
+
timeout: number;
|
|
13
|
+
}): {
|
|
14
|
+
saves: Saves;
|
|
15
|
+
opened: Promise<void>;
|
|
16
|
+
};
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// Saves, the game's side: `arcadible.saves`, and its memory copy of the
|
|
2
|
+
// game's saves, so reads after `ready` answer at once.
|
|
3
|
+
import { ArcadibleError, Codes, utf8Size } from "../bus/protocol.js";
|
|
4
|
+
import { checkName, checkWrite, } from "./rules.js";
|
|
5
|
+
// [function]
|
|
6
|
+
// a call that failed this way may still have been kept by the page.
|
|
7
|
+
function mayHaveLanded(err) {
|
|
8
|
+
return err instanceof ArcadibleError && err.code === Codes.Timeout;
|
|
9
|
+
}
|
|
10
|
+
// [function]
|
|
11
|
+
function infoOf(meta) {
|
|
12
|
+
return { version: meta.version, savedAt: meta.savedAt };
|
|
13
|
+
}
|
|
14
|
+
// [function]
|
|
15
|
+
// The saves module on `bus`. `opened` settles once the game's saves have
|
|
16
|
+
// arrived, the channel's first message, or after `timeout` if they haven't,
|
|
17
|
+
// since `ready` never fails.
|
|
18
|
+
export function createSaves(bus, { timeout }) {
|
|
19
|
+
// whether `opened` has settled: the sync calls answer from then.
|
|
20
|
+
let settled = false;
|
|
21
|
+
// whether the memory copy holds the game's saves.
|
|
22
|
+
let loaded = false;
|
|
23
|
+
let version = null;
|
|
24
|
+
const mirror = new Map();
|
|
25
|
+
// the newest revision seen of each save, removals included.
|
|
26
|
+
const revisions = new Map();
|
|
27
|
+
const early = [];
|
|
28
|
+
let open = () => undefined;
|
|
29
|
+
const opened = new Promise((resolve) => {
|
|
30
|
+
open = () => {
|
|
31
|
+
settled = true;
|
|
32
|
+
resolve();
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
// [function]
|
|
36
|
+
// what the page kept for `name`, unless a newer change got there first:
|
|
37
|
+
// changes can arrive out of order. Whether it was newer.
|
|
38
|
+
const kept = function (name, revision, save) {
|
|
39
|
+
if (revision <= (revisions.get(name) ?? 0))
|
|
40
|
+
return false;
|
|
41
|
+
revisions.set(name, revision);
|
|
42
|
+
if (save)
|
|
43
|
+
mirror.set(name, save);
|
|
44
|
+
else
|
|
45
|
+
mirror.delete(name);
|
|
46
|
+
return true;
|
|
47
|
+
};
|
|
48
|
+
// [function]
|
|
49
|
+
// another tab's change (later, another device's): the memory copy keeps
|
|
50
|
+
// up, so the next read sees it.
|
|
51
|
+
const apply = function ({ name, save, revision }) {
|
|
52
|
+
kept(name, revision, save);
|
|
53
|
+
};
|
|
54
|
+
// [function]
|
|
55
|
+
const restore = function (name, previous) {
|
|
56
|
+
if (previous)
|
|
57
|
+
mirror.set(name, previous);
|
|
58
|
+
else
|
|
59
|
+
mirror.delete(name);
|
|
60
|
+
};
|
|
61
|
+
// [function]
|
|
62
|
+
const assertReady = function () {
|
|
63
|
+
if (!settled) {
|
|
64
|
+
throw new ArcadibleError(Codes.NotReady, "Saves aren't loaded yet. Wait for arcadible.ready first.");
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
// [function]
|
|
68
|
+
const assertLoaded = function () {
|
|
69
|
+
assertReady();
|
|
70
|
+
if (!loaded) {
|
|
71
|
+
throw new ArcadibleError(Codes.Unavailable, "Saves haven't loaded yet. Try again, or use the async calls.");
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
// [function]
|
|
75
|
+
const request = async function (name, params) {
|
|
76
|
+
await opened;
|
|
77
|
+
return bus.request(name, params);
|
|
78
|
+
};
|
|
79
|
+
// [start]
|
|
80
|
+
let timer;
|
|
81
|
+
bus.on("saves.load", (data) => {
|
|
82
|
+
if (loaded)
|
|
83
|
+
return;
|
|
84
|
+
clearTimeout(timer);
|
|
85
|
+
const load = data;
|
|
86
|
+
try {
|
|
87
|
+
if (!load?.error && Array.isArray(load?.items)) {
|
|
88
|
+
for (const save of load.items) {
|
|
89
|
+
mirror.set(save.name, save);
|
|
90
|
+
revisions.set(save.name, save.info.revision);
|
|
91
|
+
}
|
|
92
|
+
loaded = true;
|
|
93
|
+
for (const change of early.splice(0))
|
|
94
|
+
apply(change);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
// even a malformed load settles `ready`.
|
|
99
|
+
open();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
bus.on("saves.change", (data) => {
|
|
103
|
+
// one that comes before the saves is kept until they do.
|
|
104
|
+
if (loaded)
|
|
105
|
+
apply(data);
|
|
106
|
+
else
|
|
107
|
+
early.push(data);
|
|
108
|
+
});
|
|
109
|
+
bus.welcomed.then((welcome) => {
|
|
110
|
+
version = welcome.build?.version ?? null;
|
|
111
|
+
// the memory copy doesn't pretend to be empty while the saves are late:
|
|
112
|
+
// a game that took that for "no save yet" could start over, and
|
|
113
|
+
// overwrite the player's progress once they arrived.
|
|
114
|
+
if (!settled) {
|
|
115
|
+
timer = setTimeout(() => {
|
|
116
|
+
console.warn("arcadible: the game's saves haven't arrived yet.");
|
|
117
|
+
open();
|
|
118
|
+
}, timeout);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
const saves = {
|
|
122
|
+
async write(name, data) {
|
|
123
|
+
await opened;
|
|
124
|
+
// before the saves have loaded, only the name and document can be
|
|
125
|
+
// checked here; the page checks the rest once they have.
|
|
126
|
+
const optimistic = loaded;
|
|
127
|
+
const refused = checkWrite(optimistic ? mirror : new Map(), name, data);
|
|
128
|
+
if (refused)
|
|
129
|
+
throw refused;
|
|
130
|
+
// the memory copy changes at once, so the next read sees it; the
|
|
131
|
+
// promise settles once the page has kept it.
|
|
132
|
+
const previous = mirror.get(name);
|
|
133
|
+
if (optimistic) {
|
|
134
|
+
mirror.set(name, {
|
|
135
|
+
name,
|
|
136
|
+
data,
|
|
137
|
+
info: {
|
|
138
|
+
version,
|
|
139
|
+
savedAt: new Date().toISOString(),
|
|
140
|
+
size: utf8Size(data),
|
|
141
|
+
revision: previous?.info.revision ?? 0,
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
const meta = (await bus.request("saves.write", {
|
|
147
|
+
name,
|
|
148
|
+
data,
|
|
149
|
+
}));
|
|
150
|
+
kept(name, meta.revision, { name, data, info: meta });
|
|
151
|
+
return infoOf(meta);
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
// one the page refused is rolled back, unless something newer
|
|
155
|
+
// replaced it meanwhile.
|
|
156
|
+
if (optimistic &&
|
|
157
|
+
!mayHaveLanded(err) &&
|
|
158
|
+
mirror.get(name)?.data === data) {
|
|
159
|
+
restore(name, previous);
|
|
160
|
+
}
|
|
161
|
+
throw err;
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
async read(name) {
|
|
165
|
+
const refused = checkName(name);
|
|
166
|
+
if (refused)
|
|
167
|
+
throw refused;
|
|
168
|
+
return (await request("saves.read", { name }));
|
|
169
|
+
},
|
|
170
|
+
async list() {
|
|
171
|
+
return (await request("saves.list"));
|
|
172
|
+
},
|
|
173
|
+
async info(name) {
|
|
174
|
+
const refused = checkName(name);
|
|
175
|
+
if (refused)
|
|
176
|
+
throw refused;
|
|
177
|
+
const meta = (await request("saves.info", { name }));
|
|
178
|
+
return meta ? infoOf(meta) : null;
|
|
179
|
+
},
|
|
180
|
+
async remove(name) {
|
|
181
|
+
const refused = checkName(name);
|
|
182
|
+
if (refused)
|
|
183
|
+
throw refused;
|
|
184
|
+
await opened;
|
|
185
|
+
const optimistic = loaded;
|
|
186
|
+
const previous = mirror.get(name);
|
|
187
|
+
if (optimistic)
|
|
188
|
+
mirror.delete(name);
|
|
189
|
+
try {
|
|
190
|
+
const revision = (await bus.request("saves.remove", { name }));
|
|
191
|
+
if (revision !== null)
|
|
192
|
+
kept(name, revision, null);
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
if (optimistic && !mayHaveLanded(err) && !mirror.has(name)) {
|
|
196
|
+
restore(name, previous);
|
|
197
|
+
}
|
|
198
|
+
throw err;
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
readSync(name) {
|
|
202
|
+
assertLoaded();
|
|
203
|
+
const refused = checkName(name);
|
|
204
|
+
if (refused)
|
|
205
|
+
throw refused;
|
|
206
|
+
return mirror.get(name)?.data ?? null;
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
return { saves, opened };
|
|
210
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ArcadibleError } from "../bus/protocol.js";
|
|
2
|
+
export declare const MAX_SAVES = 32;
|
|
3
|
+
export declare const MAX_TOTAL: number;
|
|
4
|
+
export type SaveStatus = "local" | "ephemeral";
|
|
5
|
+
export type SaveInfo = {
|
|
6
|
+
version: string | null;
|
|
7
|
+
savedAt: string;
|
|
8
|
+
};
|
|
9
|
+
export type SaveMeta = SaveInfo & {
|
|
10
|
+
size: number;
|
|
11
|
+
revision: number;
|
|
12
|
+
};
|
|
13
|
+
export type Save = {
|
|
14
|
+
name: string;
|
|
15
|
+
data: string;
|
|
16
|
+
info: SaveMeta;
|
|
17
|
+
};
|
|
18
|
+
export type SavesLoad = {
|
|
19
|
+
status: SaveStatus;
|
|
20
|
+
items: Save[];
|
|
21
|
+
error?: {
|
|
22
|
+
code: string;
|
|
23
|
+
message: string;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
export type SavesChange = {
|
|
27
|
+
name: string;
|
|
28
|
+
save: Save | null;
|
|
29
|
+
revision: number;
|
|
30
|
+
};
|
|
31
|
+
export declare function isWellFormed(text: string): boolean;
|
|
32
|
+
export declare function checkName(name: unknown): ArcadibleError | null;
|
|
33
|
+
export declare function checkWrite(saves: ReadonlyMap<string, {
|
|
34
|
+
info: SaveMeta;
|
|
35
|
+
}>, name: unknown, data: unknown): ArcadibleError | null;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Saves' rules, shared by the game's side and the page's so they can't drift
|
|
2
|
+
// apart: the shapes, the limits, and their checks (DESIGN.md, section 6).
|
|
3
|
+
// [import]
|
|
4
|
+
import { ArcadibleError, Codes, utf8Size } from "../bus/protocol.js";
|
|
5
|
+
// [declare]
|
|
6
|
+
// Saves' limits, the same everywhere (DESIGN.md, section 6, Limits).
|
|
7
|
+
export const MAX_SAVES = 32;
|
|
8
|
+
// Every save's document together, in UTF-8 bytes.
|
|
9
|
+
export const MAX_TOTAL = 1024 * 1024;
|
|
10
|
+
const NAME = /^[A-Za-z0-9_.-]{1,64}$/u;
|
|
11
|
+
// A lone surrogate: half a character, which storage and the network corrupt.
|
|
12
|
+
const LONE_SURROGATE = /\p{Surrogate}/u;
|
|
13
|
+
// [function]
|
|
14
|
+
export function isWellFormed(text) {
|
|
15
|
+
return !LONE_SURROGATE.test(text);
|
|
16
|
+
}
|
|
17
|
+
// [function]
|
|
18
|
+
// Why a save's name is refused, or null.
|
|
19
|
+
export function checkName(name) {
|
|
20
|
+
if (typeof name !== "string" || !NAME.test(name)) {
|
|
21
|
+
return new ArcadibleError(Codes.Invalid, "A save's name is 1 to 64 letters, digits, '-', '_', or '.'.");
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
// [function]
|
|
26
|
+
// Why a save is refused, given the saves already kept (`saves`), or null.
|
|
27
|
+
export function checkWrite(saves, name, data) {
|
|
28
|
+
const refused = checkName(name);
|
|
29
|
+
if (refused)
|
|
30
|
+
return refused;
|
|
31
|
+
if (typeof data !== "string") {
|
|
32
|
+
return new ArcadibleError(Codes.Invalid, "A save is a string: serialize it first, such as with JSON.stringify.");
|
|
33
|
+
}
|
|
34
|
+
if (!isWellFormed(data)) {
|
|
35
|
+
return new ArcadibleError(Codes.Invalid, "A save can't hold half a character (a lone surrogate).");
|
|
36
|
+
}
|
|
37
|
+
if (!saves.has(name) && saves.size >= MAX_SAVES) {
|
|
38
|
+
return new ArcadibleError(Codes.TooMany, `A game keeps at most ${MAX_SAVES} saves. Remove one, or combine them.`);
|
|
39
|
+
}
|
|
40
|
+
let total = utf8Size(data);
|
|
41
|
+
for (const [other, save] of saves) {
|
|
42
|
+
if (other !== name)
|
|
43
|
+
total += save.info.size;
|
|
44
|
+
}
|
|
45
|
+
if (total > MAX_TOTAL) {
|
|
46
|
+
return new ArcadibleError(Codes.TooLarge, `A game's saves hold at most ${MAX_TOTAL / 1024 / 1024} MB together.`);
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Module, ModuleContext } from "../bus/server.js";
|
|
2
|
+
import type { Backend, SaveRecord, Scope } from "./backend/types.js";
|
|
3
|
+
import { type Save } from "./rules.js";
|
|
4
|
+
export type SavesServerOptions = {
|
|
5
|
+
scope: Scope;
|
|
6
|
+
version: string | null;
|
|
7
|
+
backend: Backend;
|
|
8
|
+
now: () => Date;
|
|
9
|
+
};
|
|
10
|
+
export declare function toSave(record: SaveRecord): Save;
|
|
11
|
+
export declare function savesServer(options: SavesServerOptions): (context: ModuleContext) => Module;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Saves, the page's side: it answers a game's saves requests from its
|
|
2
|
+
// backend, enforces the limits, and stamps each save with the game's
|
|
3
|
+
// version. Its first message on each channel is the game's saves.
|
|
4
|
+
// [import]
|
|
5
|
+
import { ArcadibleError, Codes, utf8Size } from "../bus/protocol.js";
|
|
6
|
+
import { checkName, checkWrite, } from "./rules.js";
|
|
7
|
+
// [function]
|
|
8
|
+
export function toSave(record) {
|
|
9
|
+
const { name, data, version, savedAt, size, revision } = record;
|
|
10
|
+
return { name, data, info: { version, savedAt, size, revision } };
|
|
11
|
+
}
|
|
12
|
+
// [function]
|
|
13
|
+
// A writer's name, so the backend doesn't tell it of its own changes. It
|
|
14
|
+
// needn't be secret, and `crypto.randomUUID` isn't on plain-http pages.
|
|
15
|
+
function sourceId() {
|
|
16
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
17
|
+
}
|
|
18
|
+
// [function]
|
|
19
|
+
function nameOf(params) {
|
|
20
|
+
const refused = checkName(params["name"]);
|
|
21
|
+
if (refused)
|
|
22
|
+
throw refused;
|
|
23
|
+
return params["name"];
|
|
24
|
+
}
|
|
25
|
+
// [function]
|
|
26
|
+
// the device is out of room: retrying won't help until the player frees
|
|
27
|
+
// some. Anything else is the bus's `unavailable`.
|
|
28
|
+
function storageError(err) {
|
|
29
|
+
if (err?.name === "QuotaExceededError") {
|
|
30
|
+
return new ArcadibleError(Codes.Full, "This device has no room left for saves.");
|
|
31
|
+
}
|
|
32
|
+
return err;
|
|
33
|
+
}
|
|
34
|
+
// [function]
|
|
35
|
+
export function savesServer(options) {
|
|
36
|
+
const { backend, now, scope, version } = options;
|
|
37
|
+
const source = sourceId();
|
|
38
|
+
// a write, checked against the game's saves in the same step as it's kept.
|
|
39
|
+
const write = async function (params) {
|
|
40
|
+
const { name, data } = params;
|
|
41
|
+
const change = await backend
|
|
42
|
+
.change(scope, name, (saves) => {
|
|
43
|
+
const refused = checkWrite(new Map(saves.map((r) => [r.name, toSave(r)])), name, data);
|
|
44
|
+
if (refused)
|
|
45
|
+
throw refused;
|
|
46
|
+
return {
|
|
47
|
+
name: name,
|
|
48
|
+
data: data,
|
|
49
|
+
version,
|
|
50
|
+
savedAt: now().toISOString(),
|
|
51
|
+
size: utf8Size(data),
|
|
52
|
+
};
|
|
53
|
+
}, source)
|
|
54
|
+
.catch((err) => {
|
|
55
|
+
throw storageError(err);
|
|
56
|
+
});
|
|
57
|
+
return change?.record ? toSave(change.record).info : null;
|
|
58
|
+
};
|
|
59
|
+
const load = async function () {
|
|
60
|
+
const [list] = await Promise.all([
|
|
61
|
+
backend.list(scope),
|
|
62
|
+
backend.touch(scope, now().toISOString()),
|
|
63
|
+
]);
|
|
64
|
+
return { status: backend.status(), items: list.map(toSave) };
|
|
65
|
+
};
|
|
66
|
+
return (context) => {
|
|
67
|
+
// another tab (later, another device) changed a save: tell the game,
|
|
68
|
+
// with the change's revision, so it can ignore one that arrives late.
|
|
69
|
+
const unsubscribe = backend.subscribe(scope, (change, from) => {
|
|
70
|
+
if (from === source)
|
|
71
|
+
return;
|
|
72
|
+
context.broadcast("saves.change", {
|
|
73
|
+
name: change.name,
|
|
74
|
+
save: change.record ? toSave(change.record) : null,
|
|
75
|
+
revision: change.revision,
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
return {
|
|
79
|
+
handlers: {
|
|
80
|
+
"saves.read": {
|
|
81
|
+
run: async (params) => (await backend.get(scope, nameOf(params)))?.data ?? null,
|
|
82
|
+
},
|
|
83
|
+
"saves.info": {
|
|
84
|
+
run: async (params) => {
|
|
85
|
+
const record = await backend.get(scope, nameOf(params));
|
|
86
|
+
return record ? toSave(record).info : null;
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
"saves.list": {
|
|
90
|
+
run: async () => (await backend.list(scope)).map((record) => record.name).sort(),
|
|
91
|
+
},
|
|
92
|
+
"saves.write": { run: write, coalesce: (p) => String(p["name"]) },
|
|
93
|
+
// the removal's revision, or null when there was nothing to remove.
|
|
94
|
+
"saves.remove": {
|
|
95
|
+
run: async (params) => {
|
|
96
|
+
const change = await backend.change(scope, nameOf(params), () => null, source);
|
|
97
|
+
return change?.revision ?? null;
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
async open(send) {
|
|
102
|
+
try {
|
|
103
|
+
send("saves.load", await load());
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
console.error("arcadible: the game's saves couldn't load:", err);
|
|
107
|
+
const error = err instanceof ArcadibleError
|
|
108
|
+
? err.toJSON()
|
|
109
|
+
: { code: Codes.Unavailable, message: "Saves couldn't load." };
|
|
110
|
+
send("saves.load", {
|
|
111
|
+
status: "ephemeral",
|
|
112
|
+
items: [],
|
|
113
|
+
error,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
close: unsubscribe,
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const VERSION = "0.3.0";
|
package/dist/version.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,47 @@
|
|
|
1
1
|
{
|
|
2
|
+
"author": "Offst, Inc.",
|
|
3
|
+
"description": "The Arcadible SDK: how a game talks to Arcadible, over a bus between the game and the play page, starting with saves that survive updates.",
|
|
4
|
+
"devDependencies": {
|
|
5
|
+
"@playwright/test": "1.63.0",
|
|
6
|
+
"@types/node": "^25.0.3",
|
|
7
|
+
"esbuild": "^0.28.2",
|
|
8
|
+
"fake-indexeddb": "^6.2.5",
|
|
9
|
+
"typescript": "^5.9.3"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"!dist/play.*",
|
|
20
|
+
"!dist/v1.*"
|
|
21
|
+
],
|
|
22
|
+
"homepage": "https://developer.arcadible.com/references/arcadible-sdk",
|
|
23
|
+
"keywords": [
|
|
24
|
+
"arcadible",
|
|
25
|
+
"games",
|
|
26
|
+
"saves",
|
|
27
|
+
"sdk"
|
|
28
|
+
],
|
|
29
|
+
"license": "Apache-2.0",
|
|
2
30
|
"name": "@arcadible/sdk",
|
|
3
|
-
"
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsc && node scripts/bundle.mjs",
|
|
33
|
+
"prepack": "rm -rf dist && npm run build",
|
|
34
|
+
"test": "npm run test:unit && npm run test:browser",
|
|
35
|
+
"test:browser": "npm run build && playwright test",
|
|
36
|
+
"test:unit": "tsc && tsc -p tsconfig.test.json && node --test --test-force-exit 'dist-test/test/unit/**/*.test.js'"
|
|
37
|
+
},
|
|
38
|
+
"sideEffects": [
|
|
39
|
+
"./dist/index.js",
|
|
40
|
+
"./dist/v1.js",
|
|
41
|
+
"./src/index.ts",
|
|
42
|
+
"./src/v1.ts"
|
|
43
|
+
],
|
|
44
|
+
"type": "module",
|
|
45
|
+
"types": "./dist/index.d.ts",
|
|
46
|
+
"version": "0.3.0"
|
|
47
|
+
}
|