@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.
Files changed (92) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +314 -2
  3. package/USAGE.md +372 -0
  4. package/VALIDATION.md +93 -0
  5. package/dist/adapters/levelplay/index.d.ts +15 -0
  6. package/dist/adapters/levelplay/index.js +14 -0
  7. package/dist/cocos/Storage.d.ts +16 -0
  8. package/dist/cocos/Storage.js +16 -0
  9. package/dist/cocos/XhrJsonTransport.d.ts +8 -0
  10. package/dist/cocos/XhrJsonTransport.js +65 -0
  11. package/dist/cocos/index.d.ts +48 -0
  12. package/dist/cocos/index.js +134 -0
  13. package/dist/core/ClientOwnership.d.ts +6 -0
  14. package/dist/core/ClientOwnership.js +39 -0
  15. package/dist/core/ClientScope.d.ts +5 -0
  16. package/dist/core/ClientScope.js +34 -0
  17. package/dist/core/ConfigurationSync.d.ts +18 -0
  18. package/dist/core/ConfigurationSync.js +66 -0
  19. package/dist/core/FactEncoding.d.ts +10 -0
  20. package/dist/core/FactEncoding.js +29 -0
  21. package/dist/core/HttpError.d.ts +10 -0
  22. package/dist/core/HttpError.js +17 -0
  23. package/dist/core/OutboxStore.d.ts +31 -0
  24. package/dist/core/OutboxStore.js +228 -0
  25. package/dist/core/PersistentState.d.ts +33 -0
  26. package/dist/core/PersistentState.js +92 -0
  27. package/dist/core/PlayScopeClient.d.ts +65 -0
  28. package/dist/core/PlayScopeClient.js +422 -0
  29. package/dist/core/QueueAdmission.d.ts +4 -0
  30. package/dist/core/QueueAdmission.js +29 -0
  31. package/dist/core/index.d.ts +7 -0
  32. package/dist/core/index.js +27 -0
  33. package/dist/core/revenue.d.ts +33 -0
  34. package/dist/core/revenue.js +23 -0
  35. package/dist/core/types.d.ts +155 -0
  36. package/dist/core/types.js +2 -0
  37. package/dist/core/uuid.d.ts +1 -0
  38. package/dist/core/uuid.js +9 -0
  39. package/dist/esm/adapters/levelplay/index.js +11 -0
  40. package/dist/esm/cocos/Storage.js +12 -0
  41. package/dist/esm/cocos/XhrJsonTransport.js +61 -0
  42. package/dist/esm/cocos/index.js +115 -0
  43. package/dist/esm/core/ClientOwnership.js +34 -0
  44. package/dist/esm/core/ClientScope.js +30 -0
  45. package/dist/esm/core/ConfigurationSync.js +62 -0
  46. package/dist/esm/core/FactEncoding.js +24 -0
  47. package/dist/esm/core/HttpError.js +12 -0
  48. package/dist/esm/core/OutboxStore.js +224 -0
  49. package/dist/esm/core/PersistentState.js +85 -0
  50. package/dist/esm/core/PlayScopeClient.js +415 -0
  51. package/dist/esm/core/QueueAdmission.js +25 -0
  52. package/dist/esm/core/index.js +7 -0
  53. package/dist/esm/core/revenue.js +20 -0
  54. package/dist/esm/core/types.js +1 -0
  55. package/dist/esm/core/uuid.js +6 -0
  56. package/dist/esm/package.json +1 -0
  57. package/dist/esm/protocols/game-v1/index.js +21 -0
  58. package/dist/protocols/game-v1/index.d.ts +8 -0
  59. package/dist/protocols/game-v1/index.js +25 -0
  60. package/doc/README.md +4 -0
  61. package/doc//344/275/277/347/224/250/346/214/207/345/215/227/README.md +585 -0
  62. 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
  63. 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
  64. 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
  65. 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
  66. 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
  67. 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
  68. 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
  69. 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
  70. 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
  71. 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
  72. 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
  73. 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
  74. 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
  75. 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
  76. 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
  77. 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
  78. 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
  79. 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
  80. package/doc//344/275/277/347/224/250/346/214/207/345/215/227//347/244/272/344/276/213/CoreIntegration.ts +13 -0
  81. package/doc//344/275/277/347/224/250/346/214/207/345/215/227//347/244/272/344/276/213/LevelPlayTelemetry.ts +119 -0
  82. package/doc//344/275/277/347/224/250/346/214/207/345/215/227//347/244/272/344/276/213/PlayScopeExample.ts +46 -0
  83. package/doc//344/275/277/347/224/250/346/214/207/345/215/227//347/244/272/344/276/213/QuestGarden.ts +129 -0
  84. package/doc//344/275/277/347/224/250/346/214/207/345/215/227//347/244/272/344/276/213/README.md +23 -0
  85. 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
  86. package/doc//344/275/277/347/224/250/346/214/207/345/215/227//347/244/272/344/276/213/tsconfig.json +20 -0
  87. package/examples/cocos-minimal/PlayScopeExample.ts +27 -0
  88. package/examples/quest-garden/QuestGarden.ts +129 -0
  89. package/examples/quest-garden/README.md +180 -0
  90. package/examples/quest-garden/project-fixture.json +188 -0
  91. package/examples/quest-garden/tsconfig.json +12 -0
  92. package/package.json +73 -4
@@ -0,0 +1,33 @@
1
+ import type { TrackContext } from './types';
2
+ /** Business placement names belong to the host game. */
3
+ export type AdPlacement = string;
4
+ export interface AdRevenueFact {
5
+ /** Unique per exposure, including each banner refresh. Never use just the banner object's ID. */
6
+ exposureId: string;
7
+ placement: AdPlacement;
8
+ businessPlacement?: string;
9
+ format: 'banner' | 'rewarded' | 'interstitial' | 'app_open';
10
+ provider: string;
11
+ network: string;
12
+ adUnitId: string;
13
+ amount: number;
14
+ currency: 'USD';
15
+ precision: string;
16
+ isTest: boolean;
17
+ isMock?: boolean;
18
+ isDemo?: boolean;
19
+ country?: string;
20
+ countrySource?: string;
21
+ shownAt?: string;
22
+ adConfigRevision?: string;
23
+ }
24
+ export interface EncodedRevenueEvent {
25
+ name: string;
26
+ properties: Record<string, unknown>;
27
+ }
28
+ export type AdRevenueEncoder = (fact: Readonly<AdRevenueFact>) => EncodedRevenueEvent | null;
29
+ export interface EventSink {
30
+ track(name: string, properties: Record<string, unknown>, context?: TrackContext): boolean;
31
+ }
32
+ /** Validate the income fact; the explicit encoder owns event names and wire properties. */
33
+ export declare function enqueueAdRevenue(client: EventSink, fact: AdRevenueFact, context: TrackContext, encode: AdRevenueEncoder): boolean;
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.enqueueAdRevenue = enqueueAdRevenue;
4
+ /** Validate the income fact; the explicit encoder owns event names and wire properties. */
5
+ function enqueueAdRevenue(client, fact, context, encode) {
6
+ if (!fact || ![fact.exposureId, fact.network, fact.adUnitId, fact.provider, fact.placement].every(value => typeof value === 'string' && value.trim().length > 0) ||
7
+ !Number.isFinite(fact.amount) || fact.amount < 0 || fact.currency !== 'USD' ||
8
+ typeof fact.precision !== 'string' || typeof fact.isTest !== 'boolean')
9
+ return false;
10
+ // All fact fields are scalar. The encoder cannot mutate the source exposure or its dedup identity.
11
+ const snapshot = Object.freeze({ ...fact });
12
+ let event;
13
+ try {
14
+ event = encode(snapshot);
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ if (!event || typeof event.name !== 'string' || !event.name || !event.properties ||
20
+ typeof event.properties !== 'object' || Array.isArray(event.properties))
21
+ return false;
22
+ return client.track(event.name, event.properties, { ...context, deduplicationKey: `ad_revenue:${snapshot.provider}:${snapshot.exposureId}` });
23
+ }
@@ -0,0 +1,155 @@
1
+ /** API v1 ports. No Cocos or game imports are required by the core. */
2
+ export interface StoragePort {
3
+ /** Shared object identity for adapters wrapping the same underlying store in this JS runtime. */
4
+ readonly ownershipScope?: object;
5
+ /** null means absent. Read failures must throw; never convert them to null. */
6
+ getString(key: string): string | null;
7
+ /** Replace one key atomically; flush + read-back must expose that complete value or fail. */
8
+ setString(key: string, value: string): void;
9
+ /** Required only for segmented persistence; deletion failures must throw. */
10
+ removeString?(key: string): void;
11
+ flush(): void;
12
+ }
13
+ export interface HttpResponse {
14
+ status: number;
15
+ body: unknown;
16
+ retryAfterMs?: number;
17
+ }
18
+ export interface JsonTransport {
19
+ post(url: string, headers: Record<string, string>, body: unknown): Promise<HttpResponse>;
20
+ dispose?(): void;
21
+ }
22
+ export interface ClientContext {
23
+ appVersion: string;
24
+ buildNumber: string;
25
+ platform: 'android' | 'ios' | 'web' | 'wechat';
26
+ locale: string;
27
+ timezoneOffsetMinutes: number;
28
+ environment?: 'production' | 'test';
29
+ device?: {
30
+ type: 'android_id';
31
+ id: string;
32
+ };
33
+ }
34
+ /** Storage/delivery ownership, distinct from gameplay configuration and report attribution. */
35
+ export interface ClientScope {
36
+ baseUrl: string;
37
+ projectId: string;
38
+ environment: 'production' | 'test';
39
+ }
40
+ export interface EventFact {
41
+ eventVersion: 1;
42
+ eventId: string;
43
+ name: string;
44
+ occurredAt: string;
45
+ sessionId: string;
46
+ sequence: number;
47
+ client: ClientContext;
48
+ samplingProbability: number;
49
+ run: unknown;
50
+ properties: Record<string, unknown>;
51
+ }
52
+ export interface TrackContext {
53
+ run?: unknown;
54
+ occurredAt?: string;
55
+ /** Used once at enqueue, never when replaying persisted facts. */
56
+ transform?: (event: EventFact) => EventFact;
57
+ /** Optional bounded, persistent duplicate callback guard. */
58
+ deduplicationKey?: string;
59
+ }
60
+ export interface ConfigurationContext {
61
+ readonly cache: unknown;
62
+ request(path: string, body: unknown): Promise<unknown>;
63
+ saveCache(value: unknown): void;
64
+ }
65
+ export interface PlayScopeOptions {
66
+ baseUrl: string;
67
+ projectId: string;
68
+ projectKey: string;
69
+ client: ClientContext;
70
+ /** Override only for an existing installation's historical storage namespace. */
71
+ storageKey?: string;
72
+ bootstrapClient?: ClientContext;
73
+ onIdentityReady?: (installId: string) => boolean | void;
74
+ queuePolicy?: QueuePolicy;
75
+ /** Snapshot remains the compatibility default. Segmented data stays segmented on subsequent opens. */
76
+ persistence?: 'snapshot' | 'segmented';
77
+ /** Explicit provenance assertion when adopting an existing state with no scope metadata. */
78
+ legacyScope?: ClientScope;
79
+ /** Host validates its own config schema; the core never interprets game settings. */
80
+ restoreCache?: (saved: unknown) => unknown;
81
+ resolveConfiguration?: (context: ConfigurationContext) => Promise<number>;
82
+ }
83
+ export interface QueuePolicy {
84
+ /** Pending fact capacity. Defaults to 2000; does not evict an existing queue. */
85
+ readonly maxEvents?: number;
86
+ /** UTF-8 JSON bytes retained in pending + quarantine, excluding config/identity. Default 4 MiB. */
87
+ readonly maxBytes?: number;
88
+ /** Queue length at which this input event name is refused; reserve capacity for other events. */
89
+ readonly eventLimits?: Readonly<Record<string, number>>;
90
+ }
91
+ export interface Diagnostics {
92
+ pending: number;
93
+ rejected: number;
94
+ droppedEvents: number;
95
+ lastError: string | null;
96
+ blocked: boolean;
97
+ quarantined: number;
98
+ pendingBytes: number;
99
+ quarantinedBytes: number;
100
+ maxQueueBytes: number;
101
+ persistence: 'snapshot' | 'segmented';
102
+ cleanupPending: boolean;
103
+ }
104
+ /** Original local fact retained for inspection; never uploaded automatically again. */
105
+ export interface QuarantinedEvent {
106
+ event: EventFact;
107
+ reason: 'event_too_large' | 'http_413';
108
+ quarantinedAt: string;
109
+ }
110
+ export interface EventBatchResponse {
111
+ apiVersion: number;
112
+ acceptedEventIds: string[];
113
+ rejectedEvents: {
114
+ eventId: string;
115
+ code: string;
116
+ permanent: boolean;
117
+ }[];
118
+ }
119
+ /** Accepted facts may still be memory-only when storage is temporarily unavailable. */
120
+ export type TrackResult = {
121
+ status: 'accepted';
122
+ eventId: string;
123
+ durability: 'persisted' | 'memory';
124
+ } | {
125
+ status: 'duplicate' | 'queue_full' | 'queue_bytes_full' | 'event_too_large' | 'invalid_event' | 'collection_disabled' | 'disposed';
126
+ };
127
+ export interface FlushResult {
128
+ status: 'completed' | 'backoff' | 'blocked' | 'storage_unavailable' | 'failed' | 'disposed' | 'collection_disabled';
129
+ /** Counts only facts explicitly ACKed in this operation; quarantine is separate. */
130
+ accepted: number;
131
+ rejected: number;
132
+ quarantined: number;
133
+ pending: number;
134
+ /** True only after the current local state has passed the storage read-back check. */
135
+ durable: boolean;
136
+ retryAt: number;
137
+ error: string | null;
138
+ }
139
+ export interface ConfigurationStatus {
140
+ state: 'disabled' | 'idle' | 'syncing' | 'backoff' | 'disposed';
141
+ nextAttemptAt: number;
142
+ lastError: string | null;
143
+ }
144
+ export interface ConfigurationResult {
145
+ status: 'refreshed' | 'not_due' | 'failed' | 'disabled' | 'disposed' | 'storage_unavailable';
146
+ nextAttemptAt: number;
147
+ error: string | null;
148
+ }
149
+ export interface ClientStatus {
150
+ collection: 'enabled' | 'disabled' | 'disposed';
151
+ delivery: 'idle' | 'uploading' | 'backoff' | 'blocked' | 'storage_unavailable' | 'disposed' | 'disabled';
152
+ pendingDurability: 'persisted' | 'memory';
153
+ retryAt: number;
154
+ configuration: ConfigurationStatus;
155
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1 @@
1
+ export declare function telemetryUuid(): string;
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.telemetryUuid = telemetryUuid;
4
+ function telemetryUuid() {
5
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
6
+ const n = Math.floor(Math.random() * 16);
7
+ return (c === 'x' ? n : n & 3 | 8).toString(16);
8
+ });
9
+ }
@@ -0,0 +1,11 @@
1
+ /** Does not initialize ads or replace the game's native listener. Host supplies the original exposure context. */
2
+ export function forwardLevelPlayRevenue(sink, impression, exposure, context = {}) {
3
+ if (typeof impression.revenue !== 'number' || !Number.isFinite(impression.revenue) || impression.revenue < 0 ||
4
+ !impression.adNetwork || !impression.mediationAdUnitId || typeof impression.precision !== 'string')
5
+ return false;
6
+ const country = impression.country?.trim().toUpperCase();
7
+ return sink.trackAdRevenue({ ...exposure, provider: 'levelplay', amount: impression.revenue, currency: 'USD',
8
+ precision: impression.precision, network: impression.adNetwork, adUnitId: impression.mediationAdUnitId,
9
+ ...(country && /^[A-Z]{2}$/.test(country) ? { country, countrySource: 'levelplay_sdk' } : {}),
10
+ }, context);
11
+ }
@@ -0,0 +1,12 @@
1
+ /** Inject sys.localStorage; failures reach the outbox and prevent unsafe uploads. */
2
+ export class CocosStorage {
3
+ get ownershipScope() { return this.storage; }
4
+ constructor(storage) {
5
+ this.storage = storage;
6
+ if (storage.removeItem)
7
+ this.removeString = key => storage.removeItem(key);
8
+ }
9
+ getString(key) { return this.storage.getItem(key); }
10
+ setString(key, value) { this.storage.setItem(key, value); }
11
+ flush() { }
12
+ }
@@ -0,0 +1,61 @@
1
+ /** XMLHttpRequest is supported by browsers and Cocos JSB Android/iOS. */
2
+ export class XhrJsonTransport {
3
+ constructor() {
4
+ this.pending = new Map();
5
+ this.disposed = false;
6
+ }
7
+ post(url, headers, body) {
8
+ return new Promise((resolve, reject) => {
9
+ if (this.disposed) {
10
+ reject(new Error('disposed'));
11
+ return;
12
+ }
13
+ const xhr = new XMLHttpRequest();
14
+ const finish = () => { this.pending.delete(xhr); xhr.onload = xhr.onerror = xhr.ontimeout = xhr.onabort = null; };
15
+ const fail = (error) => { finish(); reject(error); };
16
+ this.pending.set(xhr, fail);
17
+ try {
18
+ xhr.open('POST', url, true);
19
+ xhr.timeout = 10000;
20
+ for (const [key, value] of Object.entries(headers))
21
+ xhr.setRequestHeader(key, value);
22
+ xhr.onload = () => {
23
+ try {
24
+ const rawRetry = xhr.getResponseHeader('Retry-After');
25
+ const retryAfterMs = rawRetry ? /^\d+$/.test(rawRetry) ? Number(rawRetry) * 1000 : Math.max(0, Date.parse(rawRetry) - Date.now()) : 0;
26
+ let parsed;
27
+ try {
28
+ parsed = JSON.parse(xhr.responseText);
29
+ }
30
+ catch {
31
+ parsed = null;
32
+ }
33
+ const status = xhr.status;
34
+ finish();
35
+ resolve({ status, body: parsed, retryAfterMs: Number.isFinite(retryAfterMs) ? retryAfterMs : 0 });
36
+ }
37
+ catch (error) {
38
+ fail(error);
39
+ }
40
+ };
41
+ xhr.onerror = xhr.ontimeout = xhr.onabort = () => fail(new Error('request_failed'));
42
+ xhr.send(JSON.stringify(body));
43
+ }
44
+ catch (error) {
45
+ fail(error);
46
+ }
47
+ });
48
+ }
49
+ dispose() {
50
+ this.disposed = true;
51
+ for (const [xhr, fail] of [...this.pending]) {
52
+ try {
53
+ xhr.abort();
54
+ }
55
+ catch { /* A broken abort must not strand other requests. */ }
56
+ finally {
57
+ fail(new Error('disposed'));
58
+ }
59
+ }
60
+ }
61
+ }
@@ -0,0 +1,115 @@
1
+ import { PlayScopeClient, enqueueAdRevenue } from '../core/index.js';
2
+ import { encodeGameV1AdRevenue } from '../protocols/game-v1/index.js';
3
+ import { CocosStorage } from './Storage.js';
4
+ import { XhrJsonTransport } from './XhrJsonTransport.js';
5
+ export * from '../core/index.js';
6
+ export * from './Storage.js';
7
+ export * from './XhrJsonTransport.js';
8
+ /** Host owns event semantics; this wrapper only schedules flush and releases its subscriptions. */
9
+ export class CocosPlayScope {
10
+ constructor(options, runtime) {
11
+ this.off = [];
12
+ this.elapsed = 0;
13
+ this.disposed = false;
14
+ this.revenueEncoder = options.adRevenueEncoder ?? encodeGameV1AdRevenue;
15
+ this.client = options.collectionEnabled ? new PlayScopeClient(options, new CocosStorage(runtime.storage), runtime.transport ?? new XhrJsonTransport(), runtime.now) : null;
16
+ if (this.client && runtime.lifecycle) {
17
+ try {
18
+ this.off.push(runtime.lifecycle.onHide(() => { void this.flush(); }));
19
+ this.off.push(runtime.lifecycle.onShow(() => { void this.flush(); }));
20
+ }
21
+ catch (error) {
22
+ // Preserve the registration error while releasing any partially acquired resources.
23
+ try {
24
+ this.dispose();
25
+ }
26
+ catch { /* All cleanup steps have already been attempted. */ }
27
+ throw error;
28
+ }
29
+ }
30
+ }
31
+ start() { if (!this.disposed) {
32
+ this.client?.start();
33
+ void this.flush();
34
+ } }
35
+ get installId() { return this.client?.installId ?? null; }
36
+ getDiagnostics() {
37
+ return this.client?.diagnostics ??
38
+ { pending: 0, rejected: 0, droppedEvents: 0, lastError: null, blocked: false, quarantined: 0,
39
+ pendingBytes: 0, quarantinedBytes: 0, maxQueueBytes: 0, persistence: 'snapshot', cleanupPending: false };
40
+ }
41
+ getQuarantinedEvents() { return this.client?.getQuarantinedEvents() ?? []; }
42
+ get status() {
43
+ if (this.client)
44
+ return this.client.status;
45
+ return { collection: this.disposed ? 'disposed' : 'disabled', delivery: this.disposed ? 'disposed' : 'disabled',
46
+ pendingDurability: 'persisted', retryAt: 0,
47
+ configuration: { state: this.disposed ? 'disposed' : 'disabled', nextAttemptAt: 0, lastError: null } };
48
+ }
49
+ track(name, properties = {}, context = {}) {
50
+ return this.trackWithResult(name, properties, context).status === 'accepted';
51
+ }
52
+ trackWithResult(name, properties = {}, context = {}) {
53
+ if (this.disposed)
54
+ return { status: 'disposed' };
55
+ return this.client?.trackWithResult(name, properties, context) ?? { status: 'collection_disabled' };
56
+ }
57
+ trackAdRevenue(fact, context = {}) {
58
+ return !this.disposed && !!this.client && enqueueAdRevenue(this.client, fact, context, this.revenueEncoder);
59
+ }
60
+ newSession() { if (!this.disposed)
61
+ this.client?.newSession(); }
62
+ /** Call from a persistent Cocos component's update(dt), in seconds. */
63
+ update(dt) {
64
+ if (this.disposed || !Number.isFinite(dt) || dt <= 0 || dt > 1)
65
+ return;
66
+ this.elapsed += dt;
67
+ if (this.elapsed >= 5) {
68
+ this.elapsed = 0;
69
+ void this.flush();
70
+ }
71
+ }
72
+ async flush() { if (!this.disposed)
73
+ await this.client?.flush(); }
74
+ flushWithResult() {
75
+ return this.client?.flushWithResult() ?? Promise.resolve(this.inactiveFlushResult());
76
+ }
77
+ retryUploads() {
78
+ return this.client?.retryUploads() ?? Promise.resolve(this.inactiveFlushResult());
79
+ }
80
+ refreshConfiguration() {
81
+ return this.client?.refreshConfiguration() ?? Promise.resolve({ status: this.disposed ? 'disposed' : 'disabled',
82
+ nextAttemptAt: 0, error: null });
83
+ }
84
+ inactiveFlushResult() {
85
+ return { status: this.disposed ? 'disposed' : 'collection_disabled', accepted: 0, rejected: 0, quarantined: 0,
86
+ pending: 0, durable: true, retryAt: 0, error: null };
87
+ }
88
+ dispose() {
89
+ if (this.disposed)
90
+ return;
91
+ this.disposed = true;
92
+ let failed = false, firstError;
93
+ const cleanup = (release) => {
94
+ try {
95
+ release();
96
+ }
97
+ catch (error) {
98
+ if (!failed) {
99
+ failed = true;
100
+ firstError = error;
101
+ }
102
+ }
103
+ };
104
+ for (const off of this.off.splice(0))
105
+ cleanup(off);
106
+ cleanup(() => this.client?.dispose());
107
+ if (failed)
108
+ throw firstError;
109
+ }
110
+ }
111
+ export function init(options, runtime) {
112
+ const sdk = new CocosPlayScope(options, runtime);
113
+ sdk.start();
114
+ return sdk;
115
+ }
@@ -0,0 +1,34 @@
1
+ // Symbol.for shares ownership across this package's ESM and CommonJS entries.
2
+ // The registry covers one JS runtime, not other tabs or native processes.
3
+ const registryKey = Symbol.for('playscope.storageOwners.v1');
4
+ const runtime = globalThis;
5
+ const owners = runtime[registryKey] ?? (runtime[registryKey] = new WeakMap());
6
+ export class PlayScopeOwnershipError extends Error {
7
+ constructor() {
8
+ super('PlayScope already has an active writer for this storage scope');
9
+ this.code = 'duplicate_client';
10
+ this.name = 'PlayScopeOwnershipError';
11
+ }
12
+ }
13
+ export function claimStorage(storage, key) {
14
+ const scope = storage.ownershipScope ?? storage;
15
+ let keys = owners.get(scope);
16
+ if (!keys) {
17
+ keys = new Map();
18
+ owners.set(scope, keys);
19
+ }
20
+ if (keys.has(key))
21
+ throw new PlayScopeOwnershipError();
22
+ const token = {};
23
+ keys.set(key, token);
24
+ let released = false;
25
+ return () => {
26
+ if (released)
27
+ return;
28
+ released = true;
29
+ if (keys.get(key) === token)
30
+ keys.delete(key);
31
+ if (!keys.size && owners.get(scope) === keys)
32
+ owners.delete(scope);
33
+ };
34
+ }
@@ -0,0 +1,30 @@
1
+ import { PlayScopeInitializationError } from './PersistentState.js';
2
+ export function resolveClientScope(options) {
3
+ const scope = { baseUrl: options.baseUrl, projectId: options.projectId,
4
+ environment: options.client.environment ?? 'production' };
5
+ return normalize(scope);
6
+ }
7
+ function normalize(scope) {
8
+ if (!scope || typeof scope.baseUrl !== 'string' || !scope.baseUrl.replace(/\/+$/, '') ||
9
+ typeof scope.projectId !== 'string' || !scope.projectId || !['production', 'test'].includes(scope.environment))
10
+ throw new TypeError('Invalid client scope');
11
+ return { baseUrl: scope.baseUrl.replace(/\/+$/, ''), projectId: scope.projectId, environment: scope.environment };
12
+ }
13
+ function equal(a, b) {
14
+ return a.baseUrl === b.baseUrl && a.projectId === b.projectId && a.environment === b.environment;
15
+ }
16
+ /** Never change an installation identity, token, queue, or storage key to repair a scope conflict. */
17
+ export function bindClientScope(state, expected, existed, legacy) {
18
+ if (state.scope) {
19
+ if (!equal(normalize(state.scope), expected))
20
+ throw new PlayScopeInitializationError('scope_mismatch');
21
+ return;
22
+ }
23
+ if (existed) {
24
+ if (!legacy)
25
+ throw new PlayScopeInitializationError('scope_migration_required');
26
+ if (!equal(normalize(legacy), expected))
27
+ throw new PlayScopeInitializationError('scope_mismatch');
28
+ }
29
+ state.scope = { ...expected };
30
+ }
@@ -0,0 +1,62 @@
1
+ import { PlayScopeHttpError, StorageUnavailableError } from './HttpError.js';
2
+ /** Optional scheduler with its own in-flight task, failures and Retry-After budget. */
3
+ export class ConfigurationSync {
4
+ constructor(resolve, context, now) {
5
+ this.resolve = resolve;
6
+ this.context = context;
7
+ this.now = now;
8
+ this.task = null;
9
+ this.nextAttemptAt = 0;
10
+ this.failures = 0;
11
+ this.lastError = null;
12
+ this.disposed = false;
13
+ }
14
+ get status() {
15
+ return { state: this.disposed ? 'disposed' : !this.resolve ? 'disabled' : this.task ? 'syncing' :
16
+ this.lastError && this.now() < this.nextAttemptAt ? 'backoff' : 'idle',
17
+ nextAttemptAt: this.nextAttemptAt, lastError: this.lastError };
18
+ }
19
+ refresh() {
20
+ if (this.disposed)
21
+ return Promise.resolve(this.result('disposed'));
22
+ if (!this.resolve)
23
+ return Promise.resolve(this.result('disabled'));
24
+ if (this.task)
25
+ return this.task;
26
+ if (this.now() < this.nextAttemptAt)
27
+ return Promise.resolve(this.result('not_due'));
28
+ // Defer execution until task ownership is established, including synchronous host callbacks.
29
+ const task = Promise.resolve().then(() => this.run());
30
+ this.task = task;
31
+ void task.then(() => { if (this.task === task)
32
+ this.task = null; });
33
+ return task;
34
+ }
35
+ result(status) {
36
+ return { status, nextAttemptAt: this.nextAttemptAt, error: this.lastError };
37
+ }
38
+ async run() {
39
+ try {
40
+ const context = await this.context();
41
+ if (this.disposed)
42
+ return this.result('disposed');
43
+ const refresh = await this.resolve(context);
44
+ if (this.disposed)
45
+ return this.result('disposed');
46
+ this.nextAttemptAt = this.now() + Math.max(60, Math.min(86400, Number.isFinite(refresh) && refresh > 0 ? refresh : 3600)) * 1000;
47
+ this.failures = 0;
48
+ this.lastError = null;
49
+ return this.result('refreshed');
50
+ }
51
+ catch (error) {
52
+ if (this.disposed)
53
+ return this.result('disposed');
54
+ const http = error instanceof PlayScopeHttpError ? error : null;
55
+ const storageUnavailable = error instanceof StorageUnavailableError;
56
+ this.lastError = http ? `http_${http.status}` : storageUnavailable ? 'storage_unavailable' : 'configuration_error';
57
+ this.nextAttemptAt = this.now() + Math.max(http?.retryAfterMs ?? 0, Math.min(3600000, 60000 * 2 ** Math.min(this.failures++, 6)));
58
+ return this.result(storageUnavailable ? 'storage_unavailable' : 'failed');
59
+ }
60
+ }
61
+ dispose() { this.disposed = true; }
62
+ }
@@ -0,0 +1,24 @@
1
+ /** JSB-compatible UTF-8 accounting without TextEncoder. */
2
+ export function utf8Bytes(value) {
3
+ let bytes = 0;
4
+ for (const ch of value) {
5
+ const n = ch.codePointAt(0);
6
+ bytes += n <= 0x7f ? 1 : n <= 0x7ff ? 2 : n <= 0xffff ? 3 : 4;
7
+ }
8
+ return bytes;
9
+ }
10
+ /** Only immutable, SDK-owned snapshots may be cached here. */
11
+ export class FactEncoding {
12
+ constructor() {
13
+ this.cache = new WeakMap();
14
+ }
15
+ get(value) {
16
+ let encoded = this.cache.get(value);
17
+ if (!encoded) {
18
+ const json = JSON.stringify(value);
19
+ encoded = { json, bytes: utf8Bytes(json) };
20
+ this.cache.set(value, encoded);
21
+ }
22
+ return encoded;
23
+ }
24
+ }
@@ -0,0 +1,12 @@
1
+ export class PlayScopeHttpError extends Error {
2
+ constructor(status, retryAfterMs = 0, path) {
3
+ super(`PlayScope HTTP ${status}`);
4
+ this.status = status;
5
+ this.retryAfterMs = retryAfterMs;
6
+ this.path = path;
7
+ }
8
+ }
9
+ /** Internal storage barrier, distinct from transport/protocol failures. */
10
+ export class StorageUnavailableError extends Error {
11
+ constructor() { super('storage_unavailable'); }
12
+ }