@virjay/playscope-cocos 0.0.0-stage → 0.2.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/CHANGELOG.md +30 -0
- package/README.md +314 -2
- package/USAGE.md +372 -0
- package/VALIDATION.md +93 -0
- package/dist/adapters/levelplay/index.d.ts +15 -0
- package/dist/adapters/levelplay/index.js +14 -0
- package/dist/cocos/Storage.d.ts +16 -0
- package/dist/cocos/Storage.js +16 -0
- package/dist/cocos/XhrJsonTransport.d.ts +8 -0
- package/dist/cocos/XhrJsonTransport.js +65 -0
- package/dist/cocos/index.d.ts +48 -0
- package/dist/cocos/index.js +134 -0
- package/dist/core/ClientOwnership.d.ts +6 -0
- package/dist/core/ClientOwnership.js +39 -0
- package/dist/core/ClientScope.d.ts +5 -0
- package/dist/core/ClientScope.js +34 -0
- package/dist/core/ConfigurationSync.d.ts +18 -0
- package/dist/core/ConfigurationSync.js +66 -0
- package/dist/core/FactEncoding.d.ts +10 -0
- package/dist/core/FactEncoding.js +29 -0
- package/dist/core/HttpError.d.ts +10 -0
- package/dist/core/HttpError.js +17 -0
- package/dist/core/OutboxStore.d.ts +31 -0
- package/dist/core/OutboxStore.js +228 -0
- package/dist/core/PersistentState.d.ts +33 -0
- package/dist/core/PersistentState.js +92 -0
- package/dist/core/PlayScopeClient.d.ts +65 -0
- package/dist/core/PlayScopeClient.js +422 -0
- package/dist/core/QueueAdmission.d.ts +4 -0
- package/dist/core/QueueAdmission.js +29 -0
- package/dist/core/index.d.ts +7 -0
- package/dist/core/index.js +27 -0
- package/dist/core/revenue.d.ts +33 -0
- package/dist/core/revenue.js +23 -0
- package/dist/core/types.d.ts +155 -0
- package/dist/core/types.js +2 -0
- package/dist/core/uuid.d.ts +1 -0
- package/dist/core/uuid.js +9 -0
- package/dist/esm/adapters/levelplay/index.js +11 -0
- package/dist/esm/cocos/Storage.js +12 -0
- package/dist/esm/cocos/XhrJsonTransport.js +61 -0
- package/dist/esm/cocos/index.js +115 -0
- package/dist/esm/core/ClientOwnership.js +34 -0
- package/dist/esm/core/ClientScope.js +30 -0
- package/dist/esm/core/ConfigurationSync.js +62 -0
- package/dist/esm/core/FactEncoding.js +24 -0
- package/dist/esm/core/HttpError.js +12 -0
- package/dist/esm/core/OutboxStore.js +224 -0
- package/dist/esm/core/PersistentState.js +85 -0
- package/dist/esm/core/PlayScopeClient.js +415 -0
- package/dist/esm/core/QueueAdmission.js +25 -0
- package/dist/esm/core/index.js +7 -0
- package/dist/esm/core/revenue.js +20 -0
- package/dist/esm/core/types.js +1 -0
- package/dist/esm/core/uuid.js +6 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/protocols/game-v1/index.js +21 -0
- package/dist/protocols/game-v1/index.d.ts +8 -0
- package/dist/protocols/game-v1/index.js +25 -0
- package/doc/README.md +4 -0
- package/doc//344/275/277/347/224/250/346/214/207/345/215/227/README.md +585 -0
- package/doc//344/275/277/347/224/250/346/214/207/345/215/227//345/217/202/350/200/203/350/265/204/346/226/231/0.1.1-types/adapters/levelplay/index.d.ts +15 -0
- package/doc//344/275/277/347/224/250/346/214/207/345/215/227//345/217/202/350/200/203/350/265/204/346/226/231/0.1.1-types/cocos/Storage.d.ts +13 -0
- package/doc//344/275/277/347/224/250/346/214/207/345/215/227//345/217/202/350/200/203/350/265/204/346/226/231/0.1.1-types/cocos/XhrJsonTransport.d.ts +8 -0
- package/doc//344/275/277/347/224/250/346/214/207/345/215/227//345/217/202/350/200/203/350/265/204/346/226/231/0.1.1-types/cocos/index.d.ts +38 -0
- package/doc//344/275/277/347/224/250/346/214/207/345/215/227//345/217/202/350/200/203/350/265/204/346/226/231/0.1.1-types/core/PlayScopeClient.d.ts +46 -0
- package/doc//344/275/277/347/224/250/346/214/207/345/215/227//345/217/202/350/200/203/350/265/204/346/226/231/0.1.1-types/core/index.d.ts +3 -0
- package/doc//344/275/277/347/224/250/346/214/207/345/215/227//345/217/202/350/200/203/350/265/204/346/226/231/0.1.1-types/core/revenue.d.ts +25 -0
- package/doc//344/275/277/347/224/250/346/214/207/345/215/227//345/217/202/350/200/203/350/265/204/346/226/231/0.1.1-types/core/types.d.ts +81 -0
- package/doc//344/275/277/347/224/250/346/214/207/345/215/227//345/217/202/350/200/203/350/265/204/346/226/231/exports.json +89 -0
- package/doc//344/275/277/347/224/250/346/214/207/345/215/227//345/217/202/350/200/203/350/265/204/346/226/231/public-api.d.ts +281 -0
- package/doc//344/275/277/347/224/250/346/214/207/345/215/227//345/217/202/350/200/203/350/265/204/346/226/231//346/234/215/345/212/241/347/253/257/client-context.schema.json +103 -0
- package/doc//344/275/277/347/224/250/346/214/207/345/215/227//345/217/202/350/200/203/350/265/204/346/226/231//346/234/215/345/212/241/347/253/257/game-ad-properties.json +765 -0
- package/doc//344/275/277/347/224/250/346/214/207/345/215/227//345/217/202/350/200/203/350/265/204/346/226/231//346/234/215/345/212/241/347/253/257/game-builtin-properties.json +479 -0
- package/doc//344/275/277/347/224/250/346/214/207/345/215/227//345/217/202/350/200/203/350/265/204/346/226/231//346/234/215/345/212/241/347/253/257/game-event.json +278 -0
- package/doc//344/275/277/347/224/250/346/214/207/345/215/227//345/217/202/350/200/203/350/265/204/346/226/231//346/234/215/345/212/241/347/253/257/product-events.json +2878 -0
- package/doc//344/275/277/347/224/250/346/214/207/345/215/227//345/217/202/350/200/203/350/265/204/346/226/231//346/234/215/345/212/241/347/253/257/standard-contract.json +183 -0
- package/doc//344/275/277/347/224/250/346/214/207/345/215/227//345/217/202/350/200/203/350/265/204/346/226/231//346/234/215/345/212/241/347/253/257//344/272/247/345/223/201/344/272/213/344/273/266/347/264/242/345/274/225.md +87 -0
- package/doc//344/275/277/347/224/250/346/214/207/345/215/227//345/217/202/350/200/203/350/265/204/346/226/231//346/235/245/346/272/220/344/270/216/347/211/210/346/234/254.json +35 -0
- package/doc//344/275/277/347/224/250/346/214/207/345/215/227//347/244/272/344/276/213/CoreIntegration.ts +13 -0
- package/doc//344/275/277/347/224/250/346/214/207/345/215/227//347/244/272/344/276/213/LevelPlayTelemetry.ts +119 -0
- package/doc//344/275/277/347/224/250/346/214/207/345/215/227//347/244/272/344/276/213/PlayScopeExample.ts +46 -0
- package/doc//344/275/277/347/224/250/346/214/207/345/215/227//347/244/272/344/276/213/QuestGarden.ts +129 -0
- package/doc//344/275/277/347/224/250/346/214/207/345/215/227//347/244/272/344/276/213/README.md +23 -0
- package/doc//344/275/277/347/224/250/346/214/207/345/215/227//347/244/272/344/276/213/project-fixture.json +188 -0
- package/doc//344/275/277/347/224/250/346/214/207/345/215/227//347/244/272/344/276/213/tsconfig.json +20 -0
- package/examples/cocos-minimal/PlayScopeExample.ts +27 -0
- package/examples/quest-garden/QuestGarden.ts +129 -0
- package/examples/quest-garden/README.md +180 -0
- package/examples/quest-garden/project-fixture.json +188 -0
- package/examples/quest-garden/tsconfig.json +12 -0
- package/package.json +73 -4
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { parseState, PlayScopeInitializationError } from './PersistentState.js';
|
|
2
|
+
import { utf8Bytes } from './FactEncoding.js';
|
|
3
|
+
import { telemetryUuid } from './uuid.js';
|
|
4
|
+
const KINDS = ['queue', 'quarantined'];
|
|
5
|
+
const SEGMENT_BYTES = 32 * 1024;
|
|
6
|
+
const SEGMENT_EVENTS = 32;
|
|
7
|
+
function object(v) { return !!v && typeof v === 'object' && !Array.isArray(v); }
|
|
8
|
+
function corrupt() { throw new PlayScopeInitializationError('state_corrupt'); }
|
|
9
|
+
function checksum(value) {
|
|
10
|
+
let hash = 2166136261;
|
|
11
|
+
for (let i = 0; i < value.length; i++)
|
|
12
|
+
hash = Math.imul(hash ^ value.charCodeAt(i), 16777619);
|
|
13
|
+
return (hash >>> 0).toString(16);
|
|
14
|
+
}
|
|
15
|
+
/** Copy-on-write segments and a single atomic manifest commit. The journal bounds failed-write garbage. */
|
|
16
|
+
export class OutboxStore {
|
|
17
|
+
constructor(storage, key, mode, encoding) {
|
|
18
|
+
this.storage = storage;
|
|
19
|
+
this.key = key;
|
|
20
|
+
this.encoding = encoding;
|
|
21
|
+
this.segments = [];
|
|
22
|
+
this.cleanupPending_ = false;
|
|
23
|
+
this.existed_ = false;
|
|
24
|
+
if (mode !== undefined && mode !== 'snapshot' && mode !== 'segmented')
|
|
25
|
+
throw new TypeError('Invalid persistence mode');
|
|
26
|
+
this.segmented = mode === 'segmented';
|
|
27
|
+
if (this.segmented)
|
|
28
|
+
this.requireDeletion();
|
|
29
|
+
this.prefix = `${key}:outbox:v2:`;
|
|
30
|
+
this.journalKey = `${key}:outbox:journal:v2`;
|
|
31
|
+
}
|
|
32
|
+
requireDeletion() {
|
|
33
|
+
if (typeof this.storage.removeString !== 'function')
|
|
34
|
+
throw new PlayScopeInitializationError('storage_capability_missing');
|
|
35
|
+
}
|
|
36
|
+
get mode() { return this.segmented ? 'segmented' : 'snapshot'; }
|
|
37
|
+
get cleanupPending() { return this.cleanupPending_; }
|
|
38
|
+
get hasStoredState() { return this.existed_; }
|
|
39
|
+
read(key) {
|
|
40
|
+
try {
|
|
41
|
+
return this.storage.getString(key);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
throw new PlayScopeInitializationError('storage_unavailable');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
decode(raw) { try {
|
|
48
|
+
return JSON.parse(raw);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return corrupt();
|
|
52
|
+
} }
|
|
53
|
+
manifest(raw) {
|
|
54
|
+
if (raw === null)
|
|
55
|
+
return null;
|
|
56
|
+
const value = this.decode(raw);
|
|
57
|
+
if (!object(value) || value.schemaVersion !== 2)
|
|
58
|
+
return null;
|
|
59
|
+
if (!object(value.outbox))
|
|
60
|
+
return corrupt();
|
|
61
|
+
const keys = new Set();
|
|
62
|
+
for (const kind of KINDS) {
|
|
63
|
+
const refs = value.outbox[kind];
|
|
64
|
+
if (!Array.isArray(refs))
|
|
65
|
+
return corrupt();
|
|
66
|
+
for (const ref of refs) {
|
|
67
|
+
if (!object(ref) || typeof ref.key !== 'string' || !this.isSegmentKey(ref.key, kind) || keys.has(ref.key) ||
|
|
68
|
+
!Number.isSafeInteger(ref.count) || ref.count < 1 || ref.count > SEGMENT_EVENTS ||
|
|
69
|
+
!Number.isSafeInteger(ref.bytes) || ref.bytes < 1 || typeof ref.checksum !== 'string')
|
|
70
|
+
return corrupt();
|
|
71
|
+
keys.add(ref.key);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
isSegmentKey(key, kind) {
|
|
77
|
+
if (!key.startsWith(this.prefix))
|
|
78
|
+
return false;
|
|
79
|
+
const suffix = key.slice(this.prefix.length);
|
|
80
|
+
return new RegExp(`^${kind ?? '(queue|quarantined)'}:[0-9a-f-]{36}$`, 'i').test(suffix);
|
|
81
|
+
}
|
|
82
|
+
load(now, restoreCache) {
|
|
83
|
+
const raw = this.read(this.key), manifest = this.manifest(raw);
|
|
84
|
+
this.existed_ = raw !== null;
|
|
85
|
+
if (!manifest)
|
|
86
|
+
return parseState(raw, now, restoreCache);
|
|
87
|
+
this.requireDeletion();
|
|
88
|
+
this.segmented = true;
|
|
89
|
+
const lists = { queue: [], quarantined: [] };
|
|
90
|
+
for (const kind of KINDS)
|
|
91
|
+
for (const ref of manifest.outbox[kind]) {
|
|
92
|
+
const payload = this.read(ref.key);
|
|
93
|
+
if (payload === null || utf8Bytes(payload) !== ref.bytes || checksum(payload) !== ref.checksum)
|
|
94
|
+
return corrupt();
|
|
95
|
+
const segment = this.decode(payload);
|
|
96
|
+
if (!object(segment) || segment.schemaVersion !== 2 || segment.kind !== kind ||
|
|
97
|
+
!Array.isArray(segment.items) || segment.items.length !== ref.count || !segment.items.every(object))
|
|
98
|
+
return corrupt();
|
|
99
|
+
lists[kind].push(...segment.items);
|
|
100
|
+
this.segments.push({ kind, ref, items: segment.items });
|
|
101
|
+
}
|
|
102
|
+
// Reuse the explicit v1 metadata/fact validator; no persisted fact is transformed by migration.
|
|
103
|
+
const state = parseState(JSON.stringify({ ...manifest, schemaVersion: 1, ...lists }), now, restoreCache);
|
|
104
|
+
// Bind descriptors to the validated objects returned by the legacy parser.
|
|
105
|
+
for (const kind of KINDS) {
|
|
106
|
+
let offset = 0;
|
|
107
|
+
for (const segment of this.segments.filter(segment => segment.kind === kind)) {
|
|
108
|
+
segment.items = state[kind].slice(offset, offset + segment.ref.count);
|
|
109
|
+
offset += segment.ref.count;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return state;
|
|
113
|
+
}
|
|
114
|
+
refs(manifest) {
|
|
115
|
+
return manifest ? KINDS.flatMap(kind => manifest.outbox[kind].map(ref => ref.key)) : [];
|
|
116
|
+
}
|
|
117
|
+
write(key, value) {
|
|
118
|
+
this.storage.setString(key, value);
|
|
119
|
+
this.storage.flush();
|
|
120
|
+
if (this.storage.getString(key) !== value)
|
|
121
|
+
throw new Error('storage_unavailable');
|
|
122
|
+
}
|
|
123
|
+
remove(key) {
|
|
124
|
+
this.storage.removeString(key);
|
|
125
|
+
this.storage.flush();
|
|
126
|
+
if (this.storage.getString(key) !== null)
|
|
127
|
+
throw new Error('storage_unavailable');
|
|
128
|
+
}
|
|
129
|
+
cleanup(keep) {
|
|
130
|
+
const raw = this.read(this.journalKey);
|
|
131
|
+
if (raw === null) {
|
|
132
|
+
this.cleanupPending_ = false;
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
this.cleanupPending_ = true;
|
|
136
|
+
const journal = this.decode(raw);
|
|
137
|
+
if (!object(journal) || journal.schemaVersion !== 2 || !Array.isArray(journal.keys) ||
|
|
138
|
+
!journal.keys.every(key => typeof key === 'string' && this.isSegmentKey(key)))
|
|
139
|
+
return corrupt();
|
|
140
|
+
for (const key of journal.keys)
|
|
141
|
+
if (!keep.has(key))
|
|
142
|
+
this.remove(key);
|
|
143
|
+
this.remove(this.journalKey);
|
|
144
|
+
this.cleanupPending_ = false;
|
|
145
|
+
}
|
|
146
|
+
partition(kind, items) {
|
|
147
|
+
const old = this.segments.filter(segment => segment.kind === kind);
|
|
148
|
+
const byFirst = new Map(old.map(segment => [segment.items[0], segment]));
|
|
149
|
+
const segments = [], writes = new Map();
|
|
150
|
+
let index = 0;
|
|
151
|
+
const match = (at) => {
|
|
152
|
+
const segment = byFirst.get(items[at]);
|
|
153
|
+
return segment && segment.items.every((item, offset) => items[at + offset] === item) ? segment : undefined;
|
|
154
|
+
};
|
|
155
|
+
while (index < items.length) {
|
|
156
|
+
const previous = match(index);
|
|
157
|
+
// Rebuild a small tail when appending, so one extra fact does not create a new tiny segment.
|
|
158
|
+
if (previous && (previous !== old[old.length - 1] || index + previous.items.length === items.length || previous.items.length === SEGMENT_EVENTS ||
|
|
159
|
+
previous.ref.bytes >= SEGMENT_BYTES)) {
|
|
160
|
+
segments.push(previous);
|
|
161
|
+
index += previous.items.length;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const chunk = [], encoded = [];
|
|
165
|
+
let bytes = 0;
|
|
166
|
+
while (index < items.length && chunk.length < SEGMENT_EVENTS) {
|
|
167
|
+
if (chunk.length && match(index))
|
|
168
|
+
break;
|
|
169
|
+
const fact = this.encoding.get(items[index]);
|
|
170
|
+
if (chunk.length && bytes + fact.bytes + 1 > SEGMENT_BYTES)
|
|
171
|
+
break;
|
|
172
|
+
chunk.push(items[index++]);
|
|
173
|
+
encoded.push(fact.json);
|
|
174
|
+
bytes += fact.bytes + 1;
|
|
175
|
+
}
|
|
176
|
+
const payload = `{"schemaVersion":2,"kind":"${kind}","items":[${encoded.join(',')}]}`;
|
|
177
|
+
const ref = { key: `${this.prefix}${kind}:${telemetryUuid()}`, count: chunk.length,
|
|
178
|
+
bytes: utf8Bytes(payload), checksum: checksum(payload) };
|
|
179
|
+
writes.set(ref.key, payload);
|
|
180
|
+
segments.push({ kind, ref, items: chunk });
|
|
181
|
+
}
|
|
182
|
+
return { segments, writes };
|
|
183
|
+
}
|
|
184
|
+
save(state) {
|
|
185
|
+
if (!this.segmented) {
|
|
186
|
+
this.write(this.key, JSON.stringify(state));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
// An earlier failure may have committed the root before its read-back failed.
|
|
191
|
+
// Always consult the actual root before collecting any staged or retired key.
|
|
192
|
+
const committed = this.manifest(this.read(this.key));
|
|
193
|
+
this.cleanup(new Set(this.refs(committed)));
|
|
194
|
+
const next = KINDS.map(kind => this.partition(kind, state[kind]));
|
|
195
|
+
const segments = next.flatMap(part => part.segments);
|
|
196
|
+
const writes = new Map(next.flatMap(part => [...part.writes]));
|
|
197
|
+
const retained = new Set(segments.map(segment => segment.ref.key));
|
|
198
|
+
const garbage = this.refs(committed).filter(key => !retained.has(key));
|
|
199
|
+
if (writes.size || garbage.length) {
|
|
200
|
+
// Persist intent before the first new key, retaining every old referenced key until commit.
|
|
201
|
+
this.write(this.journalKey, JSON.stringify({ schemaVersion: 2, keys: [...writes.keys(), ...garbage] }));
|
|
202
|
+
this.cleanupPending_ = true;
|
|
203
|
+
for (const [key, value] of writes)
|
|
204
|
+
this.write(key, value);
|
|
205
|
+
}
|
|
206
|
+
const { queue: _queue, quarantined: _quarantined, ...metadata } = state;
|
|
207
|
+
const root = JSON.stringify({ ...metadata, schemaVersion: 2, outbox: {
|
|
208
|
+
queue: next[0].segments.map(segment => segment.ref), quarantined: next[1].segments.map(segment => segment.ref)
|
|
209
|
+
} });
|
|
210
|
+
this.write(this.key, root);
|
|
211
|
+
this.segments = segments;
|
|
212
|
+
// GC is not part of fact durability. A failed cleanup remains journaled and must finish before another write.
|
|
213
|
+
try {
|
|
214
|
+
this.cleanup(retained);
|
|
215
|
+
}
|
|
216
|
+
catch { /* Retry cleanup before the next persistence operation. */ }
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
// Reconciliation after an uncertain commit must not reuse references that GC might have retired.
|
|
220
|
+
this.segments = [];
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { telemetryUuid } from './uuid.js';
|
|
2
|
+
/** Construction fails without overwriting stored data. Retry by constructing a new client. */
|
|
3
|
+
export class PlayScopeInitializationError extends Error {
|
|
4
|
+
constructor(code) {
|
|
5
|
+
super(`PlayScope initialization failed: ${code}`);
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.name = 'PlayScopeInitializationError';
|
|
8
|
+
this.retryable = code === 'storage_unavailable';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function object(value) {
|
|
12
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
export function isEventFact(value) {
|
|
15
|
+
return object(value) && typeof value.eventId === 'string' && value.eventId.length > 0 && typeof value.name === 'string';
|
|
16
|
+
}
|
|
17
|
+
function integer(value) { return Number.isSafeInteger(value) && value >= 0; }
|
|
18
|
+
function isClientScope(value) {
|
|
19
|
+
return object(value) && typeof value.baseUrl === 'string' && !!value.baseUrl.replace(/\/+$/, '') &&
|
|
20
|
+
typeof value.projectId === 'string' && !!value.projectId && ['production', 'test'].includes(value.environment);
|
|
21
|
+
}
|
|
22
|
+
export function loadState(storage, key, now, restoreCache) {
|
|
23
|
+
let raw;
|
|
24
|
+
try {
|
|
25
|
+
raw = storage.getString(key);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
throw new PlayScopeInitializationError('storage_unavailable');
|
|
29
|
+
}
|
|
30
|
+
return parseState(raw, now, restoreCache);
|
|
31
|
+
}
|
|
32
|
+
export function parseState(raw, now, restoreCache) {
|
|
33
|
+
// Only an explicit missing key is a new installation. Empty/invalid data is preserved.
|
|
34
|
+
let state;
|
|
35
|
+
if (raw === null) {
|
|
36
|
+
state = { schemaVersion: 1, installId: telemetryUuid(), firstOpenedAt: new Date(now()).toISOString(),
|
|
37
|
+
firstOpenTracked: false, sequence: 0, token: null, cache: null, queue: [], quarantined: [], rejected: [], droppedEvents: 0 };
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
let saved;
|
|
41
|
+
try {
|
|
42
|
+
saved = JSON.parse(raw);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
throw new PlayScopeInitializationError('state_corrupt');
|
|
46
|
+
}
|
|
47
|
+
if (!object(saved))
|
|
48
|
+
throw new PlayScopeInitializationError('state_corrupt');
|
|
49
|
+
if (saved.schemaVersion !== undefined && saved.schemaVersion !== 1)
|
|
50
|
+
throw new PlayScopeInitializationError('state_version_unsupported');
|
|
51
|
+
if (typeof saved.installId !== 'string' || !/^[0-9a-f-]{36}$/i.test(saved.installId) ||
|
|
52
|
+
typeof saved.firstOpenedAt !== 'string' || !Number.isFinite(Date.parse(saved.firstOpenedAt)) ||
|
|
53
|
+
typeof saved.firstOpenTracked !== 'boolean' || !integer(saved.sequence) ||
|
|
54
|
+
!Array.isArray(saved.queue) || !saved.queue.every(isEventFact))
|
|
55
|
+
throw new PlayScopeInitializationError('state_corrupt');
|
|
56
|
+
const quarantine = saved.quarantined ?? [];
|
|
57
|
+
if (!Array.isArray(quarantine) || !quarantine.every(value => object(value) && isEventFact(value.event) &&
|
|
58
|
+
(value.reason === 'event_too_large' || value.reason === 'http_413') && typeof value.quarantinedAt === 'string'))
|
|
59
|
+
throw new PlayScopeInitializationError('state_corrupt');
|
|
60
|
+
const token = saved.token;
|
|
61
|
+
const scope = saved.scope;
|
|
62
|
+
if (scope !== undefined && !isClientScope(scope))
|
|
63
|
+
throw new PlayScopeInitializationError('state_corrupt');
|
|
64
|
+
// Versionless state is the original SDK/BlockDrop shape. Migration adds metadata;
|
|
65
|
+
// queued facts, IDs, sequence, first-open marker and identity are never recreated.
|
|
66
|
+
state = { schemaVersion: 1, installId: saved.installId, firstOpenedAt: saved.firstOpenedAt,
|
|
67
|
+
firstOpenTracked: saved.firstOpenTracked, sequence: saved.sequence, queue: saved.queue, quarantined: quarantine,
|
|
68
|
+
token: object(token) && typeof token.accessToken === 'string' && typeof token.expiresAt === 'string' &&
|
|
69
|
+
Number.isFinite(Date.parse(token.expiresAt)) ? { accessToken: token.accessToken, expiresAt: token.expiresAt } : null,
|
|
70
|
+
cache: saved.cache ?? null, droppedEvents: integer(saved.droppedEvents) ? saved.droppedEvents : 0,
|
|
71
|
+
rejected: Array.isArray(saved.rejected) ? saved.rejected.slice(-50) : [],
|
|
72
|
+
...(Array.isArray(saved.deduplicationKeys)
|
|
73
|
+
? { deduplicationKeys: saved.deduplicationKeys.filter((key) => typeof key === 'string').slice(-2000) } : {}),
|
|
74
|
+
...(scope ? { scope: { baseUrl: scope.baseUrl, projectId: scope.projectId, environment: scope.environment } } : {}) };
|
|
75
|
+
}
|
|
76
|
+
if (restoreCache) {
|
|
77
|
+
try {
|
|
78
|
+
state.cache = restoreCache(state.cache);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
throw new PlayScopeInitializationError('cache_restore_failed');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return state;
|
|
85
|
+
}
|